From efe67761bfc6209d69520524553dd5d2405c3512 Mon Sep 17 00:00:00 2001
From: rfl-urbaniak
Date: Thu, 18 Jan 2024 16:55:43 +0100
Subject: [PATCH 001/142] updated readme
---
README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 44 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 2a5b125c..f6644d15 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,10 @@
-## Evaluating Policy Transfer via Similarity Analysis and Causal Inference
-```
-python -m venv venv
-source venv/bin/activate
-pip install -r requirements.txt
-pip install -e .
-cd tests && python -m pytest
-```
+# Evaluating Policy Transfer via Similarity Analysis and Causal Inference
+
+
+## Getting started
Welcome to the repository for [polis](http://polis.basis.ai/), developed by the [Basis Research Institute](https://www.basis.ai/) for [The Opportunity Project (TOP)](https://opportunity.census.gov/) 2023 in collaboration with the U.S. Department of Commerce. The primary goal of this project is to enhance access to data for local policymakers, facilitating more informed decision-making.
@@ -18,6 +14,43 @@ Welcome to the repository for [polis](http://polis.basis.ai/), developed by the
This is the backend repository for more advanced users. For a more pleasant frontend experience and more information, please use the [app](http://polis.basis.ai/).
+Installation
+------------
+
+**Basic Setup:**
+
+```sh
+
+ git clone git@github.com:BasisResearch/cities.git
+ cd cities
+ git checkout main
+ pip install .
+```
+
+The above will install the minimal version that's ported to [polis.basis.ai](http://polis.basis.ai)
+
+**Dev Setup:**
+
+To install dev dependencies, needed to run models, train models and run all the tests, run the following command:
+
+```sh
+pip install -e .[dev]
+```
+
+Details of which packages are available in which see `setup.py`.
+
+
+** Contributing: **
+
+Before submitting a pull request, please autoformat code and ensure that unit tests pass locally
+
+```sh
+make lint # linting
+make format # runs black and isort, including on notebooks in the docs/ folder
+make tests # linting, unit and notebook tests
+```
+
+
### The repository is structured as follows:
```
@@ -36,9 +69,12 @@ This is the backend repository for more advanced users. For a more pleasant fron
└── tests
```
+**WARNING: during the beta testing, the most recent version lives on the `staging-county-data` branch, and so do the most recent versions of the notebooks. Please switch to the branch before inspecting the notebooks.
If you're interested in downloading the data or exploring advanced features beyond the frontend, check out the `guides` folder in the `docs` directory. There, you'll find:
- `data_sources.ipynb` for information on data sources,
+- `similarity-conceptual.ipynb` for a conceptual account of how similarity comparison works.
+- `counterfactual-explained.ipynb` contains a rough explanation of how our causal model works.
- `similarity_demo.ipynb` demonstrating the use of the `DataGrabber` class for easy data acces, and of our `FipsQuery` class, which is the key tool in the similarity-focused part of the project,
- `causal_insights_demo.ipynb` for an overview of how the `CausalInsight` class can be used to explore the influence of a range of intervention variables thanks to causal inference tools we employed. [WIP]
From fc50736525cbeba62c85d9766299141465046871 Mon Sep 17 00:00:00 2001
From: Emily
Date: Thu, 18 Jan 2024 11:30:01 -0500
Subject: [PATCH 002/142] adding mailing list blurb from polis.basis.ai
---
README.md | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index f6644d15..4dfa19a9 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
## Getting started
-Welcome to the repository for [polis](http://polis.basis.ai/), developed by the [Basis Research Institute](https://www.basis.ai/) for [The Opportunity Project (TOP)](https://opportunity.census.gov/) 2023 in collaboration with the U.S. Department of Commerce. The primary goal of this project is to enhance access to data for local policymakers, facilitating more informed decision-making.
+Welcome to the repository for [polis](http://polis.basis.ai/), developed by [Basis Research Institute](https://www.basis.ai/) for [The Opportunity Project (TOP)](https://opportunity.census.gov/) 2023 in collaboration with the U.S. Department of Commerce. The primary goal of this project is to enhance access to data for local policymakers, facilitating more informed decision-making.
This is the backend repository for more advanced users. For a more pleasant frontend experience and more information, please use the [app](http://polis.basis.ai/).
@@ -69,7 +69,7 @@ make tests # linting, unit and notebook tests
└── tests
```
-**WARNING: during the beta testing, the most recent version lives on the `staging-county-data` branch, and so do the most recent versions of the notebooks. Please switch to the branch before inspecting the notebooks.
+**WARNING: during the beta testing, the most recent version lives on the `staging-county-data` git branch, and so do the most recent versions of the notebooks. Please switch to this branch before inspecting the notebooks.
If you're interested in downloading the data or exploring advanced features beyond the frontend, check out the `guides` folder in the `docs` directory. There, you'll find:
- `data_sources.ipynb` for information on data sources,
@@ -78,5 +78,15 @@ If you're interested in downloading the data or exploring advanced features beyo
- `similarity_demo.ipynb` demonstrating the use of the `DataGrabber` class for easy data acces, and of our `FipsQuery` class, which is the key tool in the similarity-focused part of the project,
- `causal_insights_demo.ipynb` for an overview of how the `CausalInsight` class can be used to explore the influence of a range of intervention variables thanks to causal inference tools we employed. [WIP]
-Feel free to dive into these resources to gain deeper insights into the capabilities of the Polis project, or to reach out if you have any comments or suggestions.
+## Interested? We'd love to hear from you.
+
+[polis](http://polis.basis.ai/) is a research tool under very active development, and we are eager to hear feedback from users in the policymaking and public administration spaces to accelerate its benefit.
+
+If you have feature requests, recommendations for new data sources, tips for how to resolve missing data issues, find bugs in the tool (they certainly exist!), or anything else, please do not hesitate to contact us at polis@basis.ai.
+
+To stay up to date on our latest features, you can subscribe to our [mailing list](https://dashboard.mailerlite.com/forms/102625/110535550672308121/share). In the near-term, we will send out a notice about our upcoming batch of improvements (including performance speedups, support for mobile, and more comprehensive tutorials), as well as an interest form for users who would like to work closely with us on case studies to make the tool most useful in their work.
+
+Lastly, we emphasize that this website is still in beta testing, and hence all predictions should be taken with a grain of salt.
+
+Acknowledgments: polis was built by Basis, a non-profit AI research organization dedicated to creating automated reasoning technology that helps solve society's most intractable problems. To learn more about us, visit https://basis.ai.
From 26865db21309e4e8e7adabf33de3223c919b6c65 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 2 Aug 2024 09:57:54 -0400
Subject: [PATCH 003/142] add code for loading raw & processed parcel data into
db
---
etl/load_parcels.py | 42 +++++++++++++++++++++++++++++++++++++++++
etl/load_raw_parcels.py | 18 ++++++++++++++++++
etl/schema.sql | 22 +++++++++++++++++++++
3 files changed, 82 insertions(+)
create mode 100644 etl/load_parcels.py
create mode 100644 etl/load_raw_parcels.py
create mode 100644 etl/schema.sql
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
new file mode 100644
index 00000000..9ce370cd
--- /dev/null
+++ b/etl/load_parcels.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+import psycopg2
+
+PARCEL_YEARS = range(2002, 2018)
+COUNTY_ID = "053"
+
+conn = psycopg2.connect(database="cities")
+cur = conn.cursor()
+
+
+with open("etl/schema.sql", "r") as f:
+ cur.execute(f.read())
+conn.commit()
+
+# select distinct geometry from all parcel tables
+distinct_geom = " union ".join(
+ f"select geom from parcel_raw_{year} where city = 'MINNEAPOLIS'"
+ for year in PARCEL_YEARS
+)
+parcel_geom_load = f"insert into parcel_geom (parcel_geom_data) {distinct_geom};"
+print("Executing:", parcel_geom_load)
+cur.execute(parcel_geom_load)
+conn.commit()
+
+# insert parcel data into parcel table
+parcel_data = " union all ".join(
+ f"""
+ select replace(pin, '{COUNTY_ID}-', ''), {year}, emv_land, emv_bldg, emv_total, nullif(year_built, 0), sale_date, sale_value, parcel_geom_id
+ from parcel_raw_{year}, parcel_geom
+ where parcel_raw_{year}.geom = parcel_geom.parcel_geom_data
+ and city = 'MINNEAPOLIS'
+ """
+ for year in PARCEL_YEARS
+)
+parcel_load = f"""
+insert into parcel (parcel_id, parcel_year, parcel_emv_land, parcel_emv_building, parcel_emv_total, parcel_year_built, parcel_sale_date, parcel_sale_value, parcel_geom_id)
+ {parcel_data}
+ """
+print("Executing:", parcel_load)
+cur.execute(parcel_load)
+conn.commit()
diff --git a/etl/load_raw_parcels.py b/etl/load_raw_parcels.py
new file mode 100644
index 00000000..efbba10c
--- /dev/null
+++ b/etl/load_raw_parcels.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+
+import glob
+import os
+
+SRID = 26915 # UTM Zone 15N
+
+for parcel_shape_dir in glob.glob(
+ "zoning/data/raw/property_values/shp_plan_regional_parcels_*/"
+):
+ year = int(parcel_shape_dir.split("/")[-2].split("_")[-1])
+ print(f"Loading parcels for year {year} from {parcel_shape_dir}")
+
+ os.system(
+ f"""
+ shp2pgsql -s {SRID} -I -d {parcel_shape_dir}Parcels{year}Hennepin.shp parcel_raw_{year} | pv -l | psql --quiet cities
+ """,
+ )
diff --git a/etl/schema.sql b/etl/schema.sql
new file mode 100644
index 00000000..48199913
--- /dev/null
+++ b/etl/schema.sql
@@ -0,0 +1,22 @@
+create extension if not exists postgis;
+
+drop table if exists parcel_geom cascade;
+create table parcel_geom (
+ parcel_geom_id serial primary key
+ , parcel_geom_data geometry
+);
+create index parcel_geom_data_idx on parcel_geom using gist(parcel_geom_data);
+
+drop table if exists parcel;
+create table parcel (
+ parcel_pk serial primary key
+ , parcel_id text
+ , parcel_year int not null
+ , parcel_emv_land numeric -- Estimated Market Value, land
+ , parcel_emv_building numeric -- Estimated Market Value, building
+ , parcel_emv_total numeric -- Estimated Market Value, total (may be more than sum of land and building)
+ , parcel_year_built int
+ , parcel_sale_date date
+ , parcel_sale_value numeric
+ , parcel_geom_id int references parcel_geom(parcel_geom_id)
+);
From 94923022e1e7846988ee9b1cfebe39ab5ba65d68 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 2 Aug 2024 13:20:54 -0400
Subject: [PATCH 004/142] add code to load raw zip code data
---
etl/load_raw_zip_codes.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 etl/load_raw_zip_codes.py
diff --git a/etl/load_raw_zip_codes.py b/etl/load_raw_zip_codes.py
new file mode 100644
index 00000000..3317f5d9
--- /dev/null
+++ b/etl/load_raw_zip_codes.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+import glob
+import os
+
+SRID = 26915 # UTM Zone 15N
+
+
+os.system(
+ f"""
+ shp2pgsql -s {SRID} -I -d zoning/data/raw/base/shp_society_census2000tiger_zcta/Census2000TigerZipCodeTabAreas.shp zip_raw_2000 | pv -l | psql --quiet cities
+ """,
+)
+
+os.system(
+ f"""
+ shp2pgsql -s {SRID} -I -d zoning/data/raw/base/shp_bdry_zip_code_tabulation_areas/zip_code_tabulation_areas.shp zip_raw_2020 | pv -l | psql --quiet cities
+ """,
+)
From e4d7eb21e67a0956d3141e98d49efc4cc403cab1 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 09:29:19 -0400
Subject: [PATCH 005/142] merge code for loading raw shapes and raw zip codes
---
etl/db.py | 2 +
etl/load_raw_parcels.py | 18 --------
etl/load_raw_shapes.py | 88 +++++++++++++++++++++++++++++++++++++++
etl/load_raw_zip_codes.py | 19 ---------
4 files changed, 90 insertions(+), 37 deletions(-)
create mode 100644 etl/db.py
delete mode 100644 etl/load_raw_parcels.py
create mode 100644 etl/load_raw_shapes.py
delete mode 100644 etl/load_raw_zip_codes.py
diff --git a/etl/db.py b/etl/db.py
new file mode 100644
index 00000000..acaa0053
--- /dev/null
+++ b/etl/db.py
@@ -0,0 +1,2 @@
+HOST = "34.123.100.76"
+USER = "postgres"
diff --git a/etl/load_raw_parcels.py b/etl/load_raw_parcels.py
deleted file mode 100644
index efbba10c..00000000
--- a/etl/load_raw_parcels.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python
-
-import glob
-import os
-
-SRID = 26915 # UTM Zone 15N
-
-for parcel_shape_dir in glob.glob(
- "zoning/data/raw/property_values/shp_plan_regional_parcels_*/"
-):
- year = int(parcel_shape_dir.split("/")[-2].split("_")[-1])
- print(f"Loading parcels for year {year} from {parcel_shape_dir}")
-
- os.system(
- f"""
- shp2pgsql -s {SRID} -I -d {parcel_shape_dir}Parcels{year}Hennepin.shp parcel_raw_{year} | pv -l | psql --quiet cities
- """,
- )
diff --git a/etl/load_raw_shapes.py b/etl/load_raw_shapes.py
new file mode 100644
index 00000000..3f6a09b1
--- /dev/null
+++ b/etl/load_raw_shapes.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+
+import glob
+import subprocess
+import logging
+import os
+
+from db import HOST, USER
+
+log = logging.getLogger(__name__)
+
+BASE_DIR = "zoning/data/raw"
+OGR2OGR_OPTS = [
+ "--config",
+ "PG_USE_COPY", # use postgres specific copy
+ "-progress",
+ "-lco",
+ "PRECISION=NO", # disable use of numeric types (required when shapefiles mis-specify numeric precision)
+ "-overwrite", # overwrite existing tables
+ "-lco",
+ "GEOMETRY_NAME=geom", # name of geometry column
+ "-nlt",
+ "PROMOTE_TO_MULTI", # promote all POLYGONs to MULTIPOLYGONs
+]
+DB_OPTS = [f"Pg:dbname=cities host={HOST} user={USER} port=5432"]
+
+# (shapefile, table_name) pairs. shapefiles are relative to BASE_DIR
+REL_SHAPES = [
+ (
+ "base/shp_society_census2000tiger_zcta/Census2000TigerZipCodeTabAreas.shp",
+ "zip_raw_2000",
+ ),
+ (
+ "base/shp_bdry_zip_code_tabulation_areas/zip_code_tabulation_areas.shp",
+ "zip_raw_2020",
+ ),
+ (
+ "base/hennepin_county_census_tracts_2018/cb_2018_27_tract_500k.shp",
+ "census_tract_raw_2018",
+ ),
+ (
+ "base/hennepin_county_census_block_groups_2018/cb_2018_27_bg_500k.shp",
+ "census_block_group_raw_2018",
+ ),
+ (
+ "base/hennepin_county_census_tracts_2023/cb_2023_27_tract_500k.shp",
+ "census_tract_raw_2023",
+ ),
+ (
+ "base/hennepin_county_census_block_groups_2023/cb_2023_27_bg_500k.shp",
+ "census_block_group_raw_2023",
+ ),
+ (
+ "commercial_permits/shp_struc_non_res_construction/NonresidentialConstruction.shp",
+ "commercial_permits_raw",
+ ),
+ (
+ "residential_permits/shp_econ_residential_building_permts/ResidentialPermits.shp",
+ "residential_permits_raw",
+ ),
+]
+
+
+def main():
+ # convert relative paths to absolute paths
+ abs_shapes = [(os.path.join(BASE_DIR, shape), table) for shape, table in REL_SHAPES]
+
+ for parcel_shape_dir in glob.glob(
+ os.path.join(BASE_DIR, "property_values/shp_plan_regional_parcels_*/")
+ ):
+ year = int(parcel_shape_dir.split("/")[-2].split("_")[-1])
+ shape = os.path.join(parcel_shape_dir, f"Parcels{year}Hennepin.shp")
+ table = f"parcel_raw_{year}"
+ abs_shapes.append((shape, table))
+
+ log.info("Loading raw shape files: %s", abs_shapes)
+ for shape, table in abs_shapes:
+ if not os.path.exists(shape):
+ log.warn("Skipping %s because it does not exist", shape)
+ continue
+
+ subprocess.check_call(
+ ["ogr2ogr"] + OGR2OGR_OPTS + ["-nln", table] + DB_OPTS + [shape]
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/etl/load_raw_zip_codes.py b/etl/load_raw_zip_codes.py
deleted file mode 100644
index 3317f5d9..00000000
--- a/etl/load_raw_zip_codes.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python
-
-import glob
-import os
-
-SRID = 26915 # UTM Zone 15N
-
-
-os.system(
- f"""
- shp2pgsql -s {SRID} -I -d zoning/data/raw/base/shp_society_census2000tiger_zcta/Census2000TigerZipCodeTabAreas.shp zip_raw_2000 | pv -l | psql --quiet cities
- """,
-)
-
-os.system(
- f"""
- shp2pgsql -s {SRID} -I -d zoning/data/raw/base/shp_bdry_zip_code_tabulation_areas/zip_code_tabulation_areas.shp zip_raw_2020 | pv -l | psql --quiet cities
- """,
-)
From d1cae373e23c7d09463c6a313d294286908be284 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 11:00:24 -0400
Subject: [PATCH 006/142] use correct srid when creating joined parcel table
---
etl/load_parcels.py | 16 +++++++++-------
etl/schema.sql | 4 +++-
2 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
index 9ce370cd..40229899 100644
--- a/etl/load_parcels.py
+++ b/etl/load_parcels.py
@@ -2,20 +2,21 @@
import psycopg2
-PARCEL_YEARS = range(2002, 2018)
+from db import HOST, USER
+
+PARCEL_YEARS = range(2002, 2024)
COUNTY_ID = "053"
-conn = psycopg2.connect(database="cities")
+conn = psycopg2.connect(host=HOST, user=USER, database="cities")
cur = conn.cursor()
-
with open("etl/schema.sql", "r") as f:
cur.execute(f.read())
conn.commit()
# select distinct geometry from all parcel tables
distinct_geom = " union ".join(
- f"select geom from parcel_raw_{year} where city = 'MINNEAPOLIS'"
+ f"select geom from parcel_raw_{year} where upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'"
for year in PARCEL_YEARS
)
parcel_geom_load = f"insert into parcel_geom (parcel_geom_data) {distinct_geom};"
@@ -26,13 +27,14 @@
# insert parcel data into parcel table
parcel_data = " union all ".join(
f"""
- select replace(pin, '{COUNTY_ID}-', ''), {year}, emv_land, emv_bldg, emv_total, nullif(year_built, 0), sale_date, sale_value, parcel_geom_id
+ select replace(pin, '{COUNTY_ID}-', ''), {year}, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom_id
from parcel_raw_{year}, parcel_geom
where parcel_raw_{year}.geom = parcel_geom.parcel_geom_data
- and city = 'MINNEAPOLIS'
+ and upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'
"""
- for year in PARCEL_YEARS
+ for year in range(2002, 2018)
)
+
parcel_load = f"""
insert into parcel (parcel_id, parcel_year, parcel_emv_land, parcel_emv_building, parcel_emv_total, parcel_year_built, parcel_sale_date, parcel_sale_value, parcel_geom_id)
{parcel_data}
diff --git a/etl/schema.sql b/etl/schema.sql
index 48199913..8f27cea4 100644
--- a/etl/schema.sql
+++ b/etl/schema.sql
@@ -3,7 +3,7 @@ create extension if not exists postgis;
drop table if exists parcel_geom cascade;
create table parcel_geom (
parcel_geom_id serial primary key
- , parcel_geom_data geometry
+ , parcel_geom_data geometry(MultiPolygon, 26915) not null
);
create index parcel_geom_data_idx on parcel_geom using gist(parcel_geom_data);
@@ -12,11 +12,13 @@ create table parcel (
parcel_pk serial primary key
, parcel_id text
, parcel_year int not null
+
, parcel_emv_land numeric -- Estimated Market Value, land
, parcel_emv_building numeric -- Estimated Market Value, building
, parcel_emv_total numeric -- Estimated Market Value, total (may be more than sum of land and building)
, parcel_year_built int
, parcel_sale_date date
, parcel_sale_value numeric
+
, parcel_geom_id int references parcel_geom(parcel_geom_id)
);
From 7cae0db02290f25b58a3f899e06ad36a0820254a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 11:06:01 -0400
Subject: [PATCH 007/142] make comments visible in db
---
etl/schema.sql | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/etl/schema.sql b/etl/schema.sql
index 8f27cea4..3543c813 100644
--- a/etl/schema.sql
+++ b/etl/schema.sql
@@ -13,12 +13,16 @@ create table parcel (
, parcel_id text
, parcel_year int not null
- , parcel_emv_land numeric -- Estimated Market Value, land
- , parcel_emv_building numeric -- Estimated Market Value, building
- , parcel_emv_total numeric -- Estimated Market Value, total (may be more than sum of land and building)
+ , parcel_emv_land numeric
+ , parcel_emv_building numeric
+ , parcel_emv_total numeric
, parcel_year_built int
, parcel_sale_date date
, parcel_sale_value numeric
, parcel_geom_id int references parcel_geom(parcel_geom_id)
);
+
+comment on column parcel.parcel_emv_land is 'Estimated Market Value, land';
+comment on column parcel.parcel_emv_building is 'Estimated Market Value, buildings';
+comment on column parcel.parcel_emv_total is 'Estimated Market Value, total (may be more than sum of land and building)';
From 936ce9f45d0701f16c31eb541249963a90256aec Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 11:16:09 -0400
Subject: [PATCH 008/142] enable logging
---
etl/load_raw_shapes.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/etl/load_raw_shapes.py b/etl/load_raw_shapes.py
index 3f6a09b1..41119a47 100644
--- a/etl/load_raw_shapes.py
+++ b/etl/load_raw_shapes.py
@@ -73,9 +73,10 @@ def main():
table = f"parcel_raw_{year}"
abs_shapes.append((shape, table))
- log.info("Loading raw shape files: %s", abs_shapes)
for shape, table in abs_shapes:
- if not os.path.exists(shape):
+ if os.path.exists(shape):
+ log.info("Loading %s into %s", shape, table)
+ else:
log.warn("Skipping %s because it does not exist", shape)
continue
@@ -85,4 +86,5 @@ def main():
if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
main()
From ad5dff511b0a923da3266c110646dd7db42ede93 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 12:10:39 -0400
Subject: [PATCH 009/142] remove table prefixes from field names
---
etl/load_parcels.py | 8 ++++----
etl/schema.sql | 33 +++++++++++++++++----------------
2 files changed, 21 insertions(+), 20 deletions(-)
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
index 40229899..6128f9ba 100644
--- a/etl/load_parcels.py
+++ b/etl/load_parcels.py
@@ -19,7 +19,7 @@
f"select geom from parcel_raw_{year} where upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'"
for year in PARCEL_YEARS
)
-parcel_geom_load = f"insert into parcel_geom (parcel_geom_data) {distinct_geom};"
+parcel_geom_load = f"insert into parcel_geom (geom) {distinct_geom};"
print("Executing:", parcel_geom_load)
cur.execute(parcel_geom_load)
conn.commit()
@@ -27,16 +27,16 @@
# insert parcel data into parcel table
parcel_data = " union all ".join(
f"""
- select replace(pin, '{COUNTY_ID}-', ''), {year}, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom_id
+ select replace(pin, '{COUNTY_ID}-', ''), {year}, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
from parcel_raw_{year}, parcel_geom
- where parcel_raw_{year}.geom = parcel_geom.parcel_geom_data
+ where parcel_raw_{year}.geom = parcel_geom.geom
and upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'
"""
for year in range(2002, 2018)
)
parcel_load = f"""
-insert into parcel (parcel_id, parcel_year, parcel_emv_land, parcel_emv_building, parcel_emv_total, parcel_year_built, parcel_sale_date, parcel_sale_value, parcel_geom_id)
+insert into parcel (pid, year, emv_land, emv_building, emv_total, year_built, sale_date, sale_value, geom_id)
{parcel_data}
"""
print("Executing:", parcel_load)
diff --git a/etl/schema.sql b/etl/schema.sql
index 3543c813..4083f332 100644
--- a/etl/schema.sql
+++ b/etl/schema.sql
@@ -2,27 +2,28 @@ create extension if not exists postgis;
drop table if exists parcel_geom cascade;
create table parcel_geom (
- parcel_geom_id serial primary key
- , parcel_geom_data geometry(MultiPolygon, 26915) not null
+ id serial primary key
+ , geom geometry(MultiPolygon, 26915) not null
);
-create index parcel_geom_data_idx on parcel_geom using gist(parcel_geom_data);
+create index parcel_geom_idx on parcel_geom using gist(geom);
drop table if exists parcel;
create table parcel (
- parcel_pk serial primary key
- , parcel_id text
- , parcel_year int not null
+ id serial primary key
+ , pid text not null
+ , year int not null
- , parcel_emv_land numeric
- , parcel_emv_building numeric
- , parcel_emv_total numeric
- , parcel_year_built int
- , parcel_sale_date date
- , parcel_sale_value numeric
+ , emv_land numeric
+ , emv_building numeric
+ , emv_total numeric
+ , year_built int
+ , sale_date date
+ , sale_value numeric
- , parcel_geom_id int references parcel_geom(parcel_geom_id)
+ , geom_id int references parcel_geom(id)
);
-comment on column parcel.parcel_emv_land is 'Estimated Market Value, land';
-comment on column parcel.parcel_emv_building is 'Estimated Market Value, buildings';
-comment on column parcel.parcel_emv_total is 'Estimated Market Value, total (may be more than sum of land and building)';
+comment on column parcel.pid is 'Municipal parcel ID';
+comment on column parcel.emv_land is 'Estimated Market Value, land';
+comment on column parcel.emv_building is 'Estimated Market Value, buildings';
+comment on column parcel.emv_total is 'Estimated Market Value, total (may be more than sum of land and building)';
From e90d83362977b2e142a76c5649ea6d0c96fcce04 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 12:10:51 -0400
Subject: [PATCH 010/142] load zip data into shared table
---
etl/load_zip.py | 7 +++++++
etl/zip_schema.sql | 8 ++++++++
2 files changed, 15 insertions(+)
create mode 100644 etl/load_zip.py
create mode 100644 etl/zip_schema.sql
diff --git a/etl/load_zip.py b/etl/load_zip.py
new file mode 100644
index 00000000..14e819d3
--- /dev/null
+++ b/etl/load_zip.py
@@ -0,0 +1,7 @@
+zip_load = """
+select zcta5ce20, 2020, geom from zip_raw_2020
+union select zcta, 2000, geom from zip_raw_2000
+"""
+print("Executing:", zip_load)
+cur.execute(zip_load)
+conn.commit()
diff --git a/etl/zip_schema.sql b/etl/zip_schema.sql
new file mode 100644
index 00000000..6521a281
--- /dev/null
+++ b/etl/zip_schema.sql
@@ -0,0 +1,8 @@
+drop table if exists zip_code;
+create table zip_code (
+ id serial primary key
+ , zip_code text not null
+ , year int not null
+ , geom geometry(MultiPolygon, 4269) not null
+);
+create index zip_code_geom_idx on zip_code using gist(geom);
From c2c3aa666d2c0b2c19b9522898865b57860f08ba Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 12:14:37 -0400
Subject: [PATCH 011/142] actually load zip data
---
etl/load_zip.py | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/etl/load_zip.py b/etl/load_zip.py
index 14e819d3..8cf966db 100644
--- a/etl/load_zip.py
+++ b/etl/load_zip.py
@@ -1,6 +1,21 @@
+import psycopg2
+
+from db import HOST, USER
+
+PARCEL_YEARS = range(2002, 2024)
+COUNTY_ID = "053"
+
+conn = psycopg2.connect(host=HOST, user=USER, database="cities")
+cur = conn.cursor()
+
+with open("etl/zip_schema.sql", "r") as f:
+ cur.execute(f.read())
+conn.commit()
+
zip_load = """
+insert into zip_code(zip_code, year, geom)
select zcta5ce20, 2020, geom from zip_raw_2020
-union select zcta, 2000, geom from zip_raw_2000
+union select zcta, 2000, ST_Transform(geom, 4269) from zip_raw_2000
"""
print("Executing:", zip_load)
cur.execute(zip_load)
From 2d8e43cb3438bf4ebe2797fe52f013b93cfbf94a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 14:09:10 -0400
Subject: [PATCH 012/142] switch from dates to validity ranges
---
etl/load_parcels.py | 4 ++--
etl/load_zip.py | 6 +++---
etl/schema.sql | 3 ++-
etl/zip_schema.sql | 2 +-
4 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
index 6128f9ba..0c7c292e 100644
--- a/etl/load_parcels.py
+++ b/etl/load_parcels.py
@@ -27,7 +27,7 @@
# insert parcel data into parcel table
parcel_data = " union all ".join(
f"""
- select replace(pin, '{COUNTY_ID}-', ''), {year}, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
+ select replace(pin, '{COUNTY_ID}-', ''), '[{year}-01-01,{year+1}-01-01)'::daterange, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
from parcel_raw_{year}, parcel_geom
where parcel_raw_{year}.geom = parcel_geom.geom
and upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'
@@ -36,7 +36,7 @@
)
parcel_load = f"""
-insert into parcel (pid, year, emv_land, emv_building, emv_total, year_built, sale_date, sale_value, geom_id)
+insert into parcel (pid, valid, emv_land, emv_building, emv_total, year_built, sale_date, sale_value, geom_id)
{parcel_data}
"""
print("Executing:", parcel_load)
diff --git a/etl/load_zip.py b/etl/load_zip.py
index 8cf966db..02a27cbc 100644
--- a/etl/load_zip.py
+++ b/etl/load_zip.py
@@ -13,9 +13,9 @@
conn.commit()
zip_load = """
-insert into zip_code(zip_code, year, geom)
-select zcta5ce20, 2020, geom from zip_raw_2020
-union select zcta, 2000, ST_Transform(geom, 4269) from zip_raw_2000
+insert into zip_code(zip_code, valid, geom)
+select zcta5ce20, '[2020-01-01,)'::daterange, geom from zip_raw_2020
+union select zcta, '[2000-01-01,2020-01-01)'::daterange, ST_Transform(geom, 4269) from zip_raw_2000
"""
print("Executing:", zip_load)
cur.execute(zip_load)
diff --git a/etl/schema.sql b/etl/schema.sql
index 4083f332..11517b64 100644
--- a/etl/schema.sql
+++ b/etl/schema.sql
@@ -11,7 +11,7 @@ drop table if exists parcel;
create table parcel (
id serial primary key
, pid text not null
- , year int not null
+ , valid daterange not null
, emv_land numeric
, emv_building numeric
@@ -23,6 +23,7 @@ create table parcel (
, geom_id int references parcel_geom(id)
);
+comment on column parcel.valid is 'Dates for which this parcel is valid';
comment on column parcel.pid is 'Municipal parcel ID';
comment on column parcel.emv_land is 'Estimated Market Value, land';
comment on column parcel.emv_building is 'Estimated Market Value, buildings';
diff --git a/etl/zip_schema.sql b/etl/zip_schema.sql
index 6521a281..4a15a2fa 100644
--- a/etl/zip_schema.sql
+++ b/etl/zip_schema.sql
@@ -2,7 +2,7 @@ drop table if exists zip_code;
create table zip_code (
id serial primary key
, zip_code text not null
- , year int not null
+ , valid daterange not null
, geom geometry(MultiPolygon, 4269) not null
);
create index zip_code_geom_idx on zip_code using gist(geom);
From ccb3b62e75d048ea5713b4df466adeb763edd9c1 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 5 Aug 2024 16:19:51 -0400
Subject: [PATCH 013/142] create parcel to zip mapping
---
etl/parcel_to_zip.sql | 54 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 etl/parcel_to_zip.sql
diff --git a/etl/parcel_to_zip.sql b/etl/parcel_to_zip.sql
new file mode 100644
index 00000000..b0efa96b
--- /dev/null
+++ b/etl/parcel_to_zip.sql
@@ -0,0 +1,54 @@
+drop table if exists parcel_zip;
+create table parcel_zip (
+ parcel_id int references parcel(id)
+ , zip_code_id int references zip_code(id)
+ , valid daterange not null
+);
+
+with
+parcel_with_geom as (
+ select parcel.id, geom_id, valid, ST_Transform(geom, 4269) as geom
+ from parcel
+ join parcel_geom on geom_id = parcel_geom.id
+),
+parcel_in_zip as ( -- easy case: one parcel in one zip code
+ select parcel.id as parcel_id,
+ zip_code.id as zip_code_id,
+ parcel.valid * zip_code.valid as valid
+ from parcel_with_geom as parcel
+ join zip_code on ST_Within(parcel.geom, zip_code.geom) and parcel.valid && zip_code.valid
+),
+parcel_not_within_zip as ( -- parcels that are not fully within any zip code
+ select *
+ from parcel_with_geom
+ where not exists (select parcel_id from parcel_in_zip where parcel_id = id)
+),
+parcel_largest_overlap as ( -- parcels that overlap multiple zip codes map to the one with the largest overlap
+ select distinct on (parcel.id)
+ parcel.id as parcel_id,
+ zip_code.id as zip_code_id,
+ parcel.valid * zip_code.valid as valid
+ from parcel_not_within_zip as parcel
+ join zip_code on ST_Intersects(parcel.geom, zip_code.geom) and parcel.valid && zip_code.valid
+ order by parcel_id, ST_Area(ST_Intersection(parcel.geom, zip_code.geom)) desc
+),
+parcel_no_overlap as ( -- parcels that do not overlap any zip code
+ select *
+ from parcel_not_within_zip
+ where not exists (select parcel_id from parcel_largest_overlap where parcel_id = id)
+),
+parcel_closest as ( -- parcels that overlap no zip codes map to the closest one
+ select distinct on (parcel.id)
+ parcel.id as parcel_id,
+ zip_code.id as zip_code_id,
+ parcel.valid * zip_code.valid as valid
+ from parcel_no_overlap as parcel
+ join zip_code on parcel.valid && zip_code.valid
+ order by parcel_id, ST_Distance(parcel.geom, zip_code.geom)
+)
+insert into parcel_zip
+select * from parcel_in_zip
+union all
+select * from parcel_largest_overlap
+union all
+select * from parcel_closest;
From 57e7609fda1dc58983e4af9cf6f37ab8db0ad7fb Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 6 Aug 2024 10:15:47 -0400
Subject: [PATCH 014/142] track why a parcel was assigned to a zip code
---
etl/parcel_to_zip.sql | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/etl/parcel_to_zip.sql b/etl/parcel_to_zip.sql
index b0efa96b..a1edac26 100644
--- a/etl/parcel_to_zip.sql
+++ b/etl/parcel_to_zip.sql
@@ -1,8 +1,12 @@
+drop type if exists parcel_zip_type;
+create type parcel_zip_type as enum ('within', 'most_overlap', 'closest');
+
drop table if exists parcel_zip;
create table parcel_zip (
parcel_id int references parcel(id)
, zip_code_id int references zip_code(id)
, valid daterange not null
+ , type parcel_zip_type not null
);
with
@@ -47,8 +51,8 @@ parcel_closest as ( -- parcels that overlap no zip codes map to the closest one
order by parcel_id, ST_Distance(parcel.geom, zip_code.geom)
)
insert into parcel_zip
-select * from parcel_in_zip
+select *, 'within'::parcel_zip_type from parcel_in_zip
union all
-select * from parcel_largest_overlap
+select *, 'most_overlap'::parcel_zip_type from parcel_largest_overlap
union all
-select * from parcel_closest;
+select *, 'closest'::parcel_zip_type from parcel_closest;
From cf4b4a0bfb98f9fbd5f747198af1dc724e5edca3 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 6 Aug 2024 11:34:19 -0400
Subject: [PATCH 015/142] reformat sql
---
.pg_format | 2 +
etl/parcel_to_zip.sql | 162 +++++++++++++++++++++++++++++-------------
etl/schema.sql | 36 ++++++----
etl/zip_schema.sql | 13 ++--
scripts/clean.sh | 4 +-
5 files changed, 145 insertions(+), 72 deletions(-)
create mode 100644 .pg_format
diff --git a/.pg_format b/.pg_format
new file mode 100644
index 00000000..2a3c25bb
--- /dev/null
+++ b/.pg_format
@@ -0,0 +1,2 @@
+keyword-case=1
+comma=start
\ No newline at end of file
diff --git a/etl/parcel_to_zip.sql b/etl/parcel_to_zip.sql
index a1edac26..669e48a1 100644
--- a/etl/parcel_to_zip.sql
+++ b/etl/parcel_to_zip.sql
@@ -1,58 +1,118 @@
drop type if exists parcel_zip_type;
-create type parcel_zip_type as enum ('within', 'most_overlap', 'closest');
+
+create type parcel_zip_type as enum (
+ 'within'
+ , 'most_overlap'
+ , 'closest'
+);
drop table if exists parcel_zip;
+
create table parcel_zip (
- parcel_id int references parcel(id)
- , zip_code_id int references zip_code(id)
- , valid daterange not null
- , type parcel_zip_type not null
+ parcel_id int references parcel (id)
+ , zip_code_id int references zip_code (id)
+ , valid daterange not null
+ , type parcel_zip_type not null
);
-with
-parcel_with_geom as (
- select parcel.id, geom_id, valid, ST_Transform(geom, 4269) as geom
- from parcel
- join parcel_geom on geom_id = parcel_geom.id
-),
-parcel_in_zip as ( -- easy case: one parcel in one zip code
- select parcel.id as parcel_id,
- zip_code.id as zip_code_id,
- parcel.valid * zip_code.valid as valid
- from parcel_with_geom as parcel
- join zip_code on ST_Within(parcel.geom, zip_code.geom) and parcel.valid && zip_code.valid
-),
-parcel_not_within_zip as ( -- parcels that are not fully within any zip code
- select *
- from parcel_with_geom
- where not exists (select parcel_id from parcel_in_zip where parcel_id = id)
-),
-parcel_largest_overlap as ( -- parcels that overlap multiple zip codes map to the one with the largest overlap
- select distinct on (parcel.id)
- parcel.id as parcel_id,
- zip_code.id as zip_code_id,
- parcel.valid * zip_code.valid as valid
- from parcel_not_within_zip as parcel
- join zip_code on ST_Intersects(parcel.geom, zip_code.geom) and parcel.valid && zip_code.valid
- order by parcel_id, ST_Area(ST_Intersection(parcel.geom, zip_code.geom)) desc
-),
-parcel_no_overlap as ( -- parcels that do not overlap any zip code
- select *
- from parcel_not_within_zip
- where not exists (select parcel_id from parcel_largest_overlap where parcel_id = id)
-),
-parcel_closest as ( -- parcels that overlap no zip codes map to the closest one
- select distinct on (parcel.id)
- parcel.id as parcel_id,
- zip_code.id as zip_code_id,
- parcel.valid * zip_code.valid as valid
- from parcel_no_overlap as parcel
- join zip_code on parcel.valid && zip_code.valid
- order by parcel_id, ST_Distance(parcel.geom, zip_code.geom)
+with parcel_with_geom as (
+ select
+ parcel.id
+ , geom_id
+ , valid
+ , ST_Transform (geom
+ , 4269) as geom
+ from
+ parcel
+ join parcel_geom on geom_id = parcel_geom.id
+)
+, parcel_in_zip as (
+ -- easy case: one parcel in one zip code
+ select
+ parcel.id as parcel_id
+ , zip_code.id as zip_code_id
+ , parcel.valid * zip_code.valid as valid
+ from
+ parcel_with_geom as parcel
+ join zip_code on ST_Within (parcel.geom
+ , zip_code.geom)
+ and parcel.valid && zip_code.valid
)
-insert into parcel_zip
-select *, 'within'::parcel_zip_type from parcel_in_zip
-union all
-select *, 'most_overlap'::parcel_zip_type from parcel_largest_overlap
-union all
-select *, 'closest'::parcel_zip_type from parcel_closest;
+, parcel_not_within_zip as (
+ -- parcels that are not fully within any zip code
+ select
+ *
+ from
+ parcel_with_geom
+ where
+ not exists (
+ select
+ parcel_id
+ from
+ parcel_in_zip
+ where
+ parcel_id = id)
+)
+, parcel_largest_overlap as (
+ -- parcels that overlap multiple zip codes map to the one with the largest overlap
+ select distinct on (parcel.id)
+ parcel.id as parcel_id
+ , zip_code.id as zip_code_id
+ , parcel.valid * zip_code.valid as valid
+ from
+ parcel_not_within_zip as parcel
+ join zip_code on ST_Intersects (parcel.geom
+ , zip_code.geom)
+ and parcel.valid && zip_code.valid
+ order by
+ parcel_id
+ , ST_Area (ST_Intersection (parcel.geom
+ , zip_code.geom)) desc
+)
+, parcel_no_overlap as (
+ -- parcels that do not overlap any zip code
+ select
+ *
+ from
+ parcel_not_within_zip
+ where
+ not exists (
+ select
+ parcel_id
+ from
+ parcel_largest_overlap
+ where
+ parcel_id = id)
+)
+, parcel_closest as (
+ -- parcels that overlap no zip codes map to the closest one
+ select distinct on (parcel.id)
+ parcel.id as parcel_id
+ , zip_code.id as zip_code_id
+ , parcel.valid * zip_code.valid as valid
+ from
+ parcel_no_overlap as parcel
+ join zip_code on parcel.valid && zip_code.valid
+ order by
+ parcel_id
+ , ST_Distance (parcel.geom
+ , zip_code.geom))
+ insert into parcel_zip
+ select
+ *
+ , 'within'::parcel_zip_type
+ from
+ parcel_in_zip
+ union all
+ select
+ *
+ , 'most_overlap'::parcel_zip_type
+ from
+ parcel_largest_overlap
+ union all
+ select
+ *
+ , 'closest'::parcel_zip_type
+ from
+ parcel_closest;
+
diff --git a/etl/schema.sql b/etl/schema.sql
index 11517b64..e1ff8d38 100644
--- a/etl/schema.sql
+++ b/etl/schema.sql
@@ -1,30 +1,36 @@
create extension if not exists postgis;
drop table if exists parcel_geom cascade;
+
create table parcel_geom (
- id serial primary key
- , geom geometry(MultiPolygon, 26915) not null
+ id serial primary key
+ , geom geometry(MultiPolygon , 26915) not null
);
-create index parcel_geom_idx on parcel_geom using gist(geom);
+
+create index parcel_geom_idx on parcel_geom using gist (geom);
drop table if exists parcel;
+
create table parcel (
- id serial primary key
- , pid text not null
- , valid daterange not null
-
- , emv_land numeric
- , emv_building numeric
- , emv_total numeric
- , year_built int
- , sale_date date
- , sale_value numeric
-
- , geom_id int references parcel_geom(id)
+ id serial primary key
+ , pid text not null
+ , valid daterange not null
+ , emv_land numeric
+ , emv_building numeric
+ , emv_total numeric
+ , year_built int
+ , sale_date date
+ , sale_value numeric
+ , geom_id int references parcel_geom (id)
);
comment on column parcel.valid is 'Dates for which this parcel is valid';
+
comment on column parcel.pid is 'Municipal parcel ID';
+
comment on column parcel.emv_land is 'Estimated Market Value, land';
+
comment on column parcel.emv_building is 'Estimated Market Value, buildings';
+
comment on column parcel.emv_total is 'Estimated Market Value, total (may be more than sum of land and building)';
+
diff --git a/etl/zip_schema.sql b/etl/zip_schema.sql
index 4a15a2fa..d1a93ab7 100644
--- a/etl/zip_schema.sql
+++ b/etl/zip_schema.sql
@@ -1,8 +1,11 @@
drop table if exists zip_code;
+
create table zip_code (
- id serial primary key
- , zip_code text not null
- , valid daterange not null
- , geom geometry(MultiPolygon, 4269) not null
+ id serial primary key
+ , zip_code text not null
+ , valid daterange not null
+ , geom geometry(MultiPolygon , 4269) not null
);
-create index zip_code_geom_idx on zip_code using gist(geom);
+
+create index zip_code_geom_idx on zip_code using gist (geom);
+
diff --git a/scripts/clean.sh b/scripts/clean.sh
index 30ffad25..2bd06083 100755
--- a/scripts/clean.sh
+++ b/scripts/clean.sh
@@ -6,5 +6,7 @@ black cities/ tests/
autoflake --remove-all-unused-imports --in-place --recursive ./cities ./tests
nbqa black docs/guides/
-nbqa autoflake --remove-all-unused-imports --recursive --in-place docs/guides/
+nbqa autoflake --remove-all-unused-imports --recursive --in-place docs/guides/
nbqa isort -in-place docs/guides/
+
+pg_format -c .pg_format -i etl/*.sql
From 4fb0199f8423c0dd7c32048a6301edbb5fa3bb82 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 6 Aug 2024 11:34:37 -0400
Subject: [PATCH 016/142] process raw census data
---
etl/census_schema.sql | 87 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 87 insertions(+)
create mode 100644 etl/census_schema.sql
diff --git a/etl/census_schema.sql b/etl/census_schema.sql
new file mode 100644
index 00000000..b0716773
--- /dev/null
+++ b/etl/census_schema.sql
@@ -0,0 +1,87 @@
+drop table if exists census_tract cascade;
+
+create table census_tract (
+ id serial primary key
+ , statefp text not null
+ , countyfp text not null
+ , tractce text not null
+ , geoidfq text not null
+ , valid daterange not null
+ , geom geometry(MultiPolygon , 4269) not null
+);
+
+create index census_tract_geom_idx on census_tract using gist (geom);
+
+insert into census_tract (statefp , countyfp , tractce , geoidfq , valid , geom)
+select
+ statefp
+ , countyfp
+ , tractce
+ , affgeoid
+ , '[2010-01-01,2020-01-01)'::daterange
+ , geom
+from
+ cb_2018_27_tract_500k
+union all
+select
+ statefp
+ , countyfp
+ , tractce
+ , geoidfq
+ , '[2020-01-01,2030-01-01)'::daterange
+ , geom
+from
+ cb_2023_27_tract_500k;
+
+drop table if exists census_block_group cascade;
+
+create table census_block_group (
+ id serial primary key
+ , statefp text not null
+ , countyfp text not null
+ , tractce text not null
+ , blkgrpce text not null
+ , geoidfq text not null
+ , tract_id int references census_tract (id)
+ , valid daterange not null
+ , geom geometry(MultiPolygon , 4269) not null
+);
+
+create index census_block_group_geom_idx on census_block_group using gist (geom);
+
+insert into census_block (statefp , countyfp , tractce , blkgrpce , geoidfq , tract_id , valid , geom)
+select
+ statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , bg.geoidfq
+ , census_tract.id
+ , bg.valid
+ , bg.geom
+from (
+ select
+ statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , affgeoid as geoidfq
+ , '[2010-01-01,2020-01-01)'::daterange as valid
+ , geom
+ from
+ cb_2018_27_bg_500k
+ union all
+ select
+ statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , geoidfq
+ , '[2020-01-01,2030-01-01)'::daterange as valid
+ , geom
+ from
+ cb_2023_27_bg_500k) as bg
+ join census_tract using (statefp , countyfp , tractce)
+where
+ census_tract.valid && bg.valid;
+
From 70a1a34963356205b962c0201c2d6d97f47ef357 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 6 Aug 2024 12:10:56 -0400
Subject: [PATCH 017/142] add census schema and mapping from parcels to block
groups
---
etl/census_schema.sql | 13 +++--
etl/parcel_to_bg.sql | 117 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 125 insertions(+), 5 deletions(-)
create mode 100644 etl/parcel_to_bg.sql
diff --git a/etl/census_schema.sql b/etl/census_schema.sql
index b0716773..521bd20b 100644
--- a/etl/census_schema.sql
+++ b/etl/census_schema.sql
@@ -12,6 +12,8 @@ create table census_tract (
create index census_tract_geom_idx on census_tract using gist (geom);
+create index census_tract_valid_idx on census_tract using gist (valid);
+
insert into census_tract (statefp , countyfp , tractce , geoidfq , valid , geom)
select
statefp
@@ -33,9 +35,9 @@ select
from
cb_2023_27_tract_500k;
-drop table if exists census_block_group cascade;
+drop table if exists census_bg cascade;
-create table census_block_group (
+create table census_bg (
id serial primary key
, statefp text not null
, countyfp text not null
@@ -47,9 +49,11 @@ create table census_block_group (
, geom geometry(MultiPolygon , 4269) not null
);
-create index census_block_group_geom_idx on census_block_group using gist (geom);
+create index census_bg_geom_idx on census_bg using gist (geom);
+
+create index census_bg_valid_idx on census_bg using gist (valid);
-insert into census_block (statefp , countyfp , tractce , blkgrpce , geoidfq , tract_id , valid , geom)
+insert into census_bg (statefp , countyfp , tractce , blkgrpce , geoidfq , tract_id , valid , geom)
select
statefp
, countyfp
@@ -84,4 +88,3 @@ from (
join census_tract using (statefp , countyfp , tractce)
where
census_tract.valid && bg.valid;
-
diff --git a/etl/parcel_to_bg.sql b/etl/parcel_to_bg.sql
new file mode 100644
index 00000000..ebee0dde
--- /dev/null
+++ b/etl/parcel_to_bg.sql
@@ -0,0 +1,117 @@
+drop type if exists parcel_census_bg_type cascade;
+
+create type parcel_census_bg_type as enum (
+ 'within'
+ , 'most_overlap'
+ , 'closest'
+);
+
+drop table if exists parcel_census_bg;
+
+create table parcel_census_bg (
+ parcel_id int references parcel (id)
+ , census_bg_id int references census_bg (id)
+ , valid daterange not null
+ , type parcel_census_bg_type not null
+);
+
+with parcel_with_geom as (
+ select
+ parcel.id
+ , geom_id
+ , valid
+ , ST_Transform (geom
+ , 4269) as geom
+ from
+ parcel
+ join parcel_geom on geom_id = parcel_geom.id
+)
+, parcel_within as (
+ -- easy case: one parcel in one bg
+ select
+ parcel.id as parcel_id
+ , census_bg.id as census_bg_id
+ , parcel.valid * census_bg.valid as valid
+ from
+ parcel_with_geom as parcel
+ join census_bg on ST_Within (parcel.geom
+ , census_bg.geom)
+ and parcel.valid && census_bg.valid
+)
+, parcel_not_within as (
+ -- parcels that are not fully within any bg
+ select
+ *
+ from
+ parcel_with_geom
+ where
+ not exists (
+ select
+ parcel_id
+ from
+ parcel_within
+ where
+ parcel_id = id)
+)
+, parcel_largest_overlap as (
+ -- parcels that overlap multiple bgs map to the one with the largest overlap
+ select distinct on (parcel.id)
+ parcel.id as parcel_id
+ , census_bg.id as census_bg_id
+ , parcel.valid * census_bg.valid as valid
+ from
+ parcel_not_within as parcel
+ join census_bg on ST_Intersects (parcel.geom
+ , census_bg.geom)
+ and parcel.valid && census_bg.valid
+ order by
+ parcel_id
+ , ST_Area (ST_Intersection (parcel.geom
+ , census_bg.geom)) desc
+)
+, parcel_no_overlap as (
+ -- parcels that do not overlap any bg
+ select
+ *
+ from
+ parcel_not_within
+ where
+ not exists (
+ select
+ parcel_id
+ from
+ parcel_largest_overlap
+ where
+ parcel_id = id)
+)
+, parcel_closest as (
+ -- parcels that overlap no bgs map to the closest one
+ select distinct on (parcel.id)
+ parcel.id as parcel_id
+ , census_bg.id as census_bg_id
+ , parcel.valid * census_bg.valid as valid
+ from
+ parcel_no_overlap as parcel
+ join census_bg on parcel.valid && census_bg.valid
+ order by
+ parcel_id
+ , ST_Distance (parcel.geom
+ , census_bg.geom))
+ insert into parcel_census_bg
+ select
+ *
+ , 'within'::parcel_census_bg_type
+ from
+ parcel_within
+ union all
+ select
+ *
+ , 'most_overlap'::parcel_census_bg_type
+ from
+ parcel_largest_overlap
+ union all
+ select
+ *
+ , 'closest'::parcel_census_bg_type
+ from
+ parcel_closest;
From 648f22c68aec63000b847bc28186eea713059403 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 6 Aug 2024 14:46:05 -0400
Subject: [PATCH 018/142] fix parcel validities
---
etl/load_parcels.py | 4 ++--
etl/schema.sql | 3 +--
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
index 0c7c292e..6604281f 100644
--- a/etl/load_parcels.py
+++ b/etl/load_parcels.py
@@ -27,12 +27,12 @@
# insert parcel data into parcel table
parcel_data = " union all ".join(
f"""
- select replace(pin, '{COUNTY_ID}-', ''), '[{year}-01-01,{year+1}-01-01)'::daterange, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
+ select replace(pin, '{COUNTY_ID}-', ''), '[{year-1}-01-01,{year}-01-01)'::daterange, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
from parcel_raw_{year}, parcel_geom
where parcel_raw_{year}.geom = parcel_geom.geom
and upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'
"""
- for year in range(2002, 2018)
+ for year in PARCEL_YEARS
)
parcel_load = f"""
diff --git a/etl/schema.sql b/etl/schema.sql
index e1ff8d38..1029c451 100644
--- a/etl/schema.sql
+++ b/etl/schema.sql
@@ -9,7 +9,7 @@ create table parcel_geom (
create index parcel_geom_idx on parcel_geom using gist (geom);
-drop table if exists parcel;
+drop table if exists parcel cascade;
create table parcel (
id serial primary key
@@ -33,4 +33,3 @@ comment on column parcel.emv_land is 'Estimated Market Value, land';
comment on column parcel.emv_building is 'Estimated Market Value, buildings';
comment on column parcel.emv_total is 'Estimated Market Value, total (may be more than sum of land and building)';
-
From 56a0b9881dfa35b6c07dcb6ac91717988a5b1836 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 6 Aug 2024 17:44:06 -0400
Subject: [PATCH 019/142] add code to process permits and match them to parcels
---
etl/permit_schema.sql | 290 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 290 insertions(+)
create mode 100644 etl/permit_schema.sql
diff --git a/etl/permit_schema.sql b/etl/permit_schema.sql
new file mode 100644
index 00000000..e1dc5df2
--- /dev/null
+++ b/etl/permit_schema.sql
@@ -0,0 +1,290 @@
+drop table if exists residential_permit cascade;
+
+create table residential_permit (
+ id serial primary key,
+ ctu_id text,
+ coctu_id text,
+ year int,
+ tenure text,
+ housing_ty text,
+ res_permit text,
+ address text,
+ zip_code text,
+ name text,
+ buildings int,
+ units int,
+ age_restri int,
+ memory_car int,
+ assisted int,
+ com_off_re boolean,
+ sqf numeric,
+ public_fun boolean,
+ permit_val numeric,
+ community_ text,
+ notes text,
+ pin text,
+ geom geometry (multipoint, 26915)
+);
+
+create index residential_permit_geom_idx on residential_permit using gist (
+ geom
+);
+
+insert into residential_permit (
+ ctu_id,
+ coctu_id,
+ year,
+ tenure,
+ housing_ty,
+ res_permit,
+ address,
+ zip_code,
+ name,
+ buildings,
+ units,
+ age_restri,
+ memory_car,
+ assisted,
+ com_off_re,
+ sqf,
+ public_fun,
+ permit_val,
+ community_,
+ notes,
+ pin,
+ geom
+)
+select
+ ctu_id,
+ coctu_id,
+ year::int,
+ tenure,
+ housing_ty,
+ res_permit,
+ address,
+ zip_code,
+ name,
+ buildings,
+ units,
+ age_restri,
+ memory_car,
+ assisted,
+ com_off_re = 'Y',
+ sqf,
+ public_fun = 'Y',
+ permit_val,
+ community_,
+ notes,
+ pin,
+ geom
+from
+ residential_permits_raw
+where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis';
+
+drop table if exists residential_permit_parcel;
+
+create table residential_permit_parcel (
+ permit_id int references residential_permit (id),
+ parcel_id int references parcel (id),
+ type_ region_tag_type
+);
+
+with within as (
+ select
+ residential_permit.id as permit_id,
+ parcel.id as parcel_id
+ from
+ parcel_with_geom as parcel
+ join residential_permit on st_within(
+ residential_permit.geom,
+ parcel.geom
+ )
+ and to_date(
+ year::text,
+ 'YYYY'
+ ) <@ parcel.valid
+),
+not_within as (
+ select
+ id,
+ year,
+ geom
+ from
+ residential_permit
+ where
+ not exists (
+ select permit_id
+ from
+ within
+ where
+ permit_id = id
+ )
+),
+closest as (
+ select distinct on (permit.id)
+ permit.id as permit_id,
+ parcel.id as parcel_id
+ from
+ not_within as permit
+ join parcel_with_geom as parcel
+ on st_dwithin(permit.geom, parcel.geom, 100.0) and to_date(
+ year::text,
+ 'YYYY'
+ ) <@ parcel.valid
+ order by
+ permit_id,
+ st_distance(
+ permit.geom,
+ parcel.geom
+ )
+)
+insert into residential_permit_parcel select
+ permit_id,
+ parcel_id,
+ 'within'::region_tag_type
+from
+ within
+union all
+select
+ permit_id,
+ parcel_id,
+ 'closest'::region_tag_type
+from
+ closest;
+
+drop table if exists commercial_permit cascade;
+
+create table commercial_permit (
+ id serial primary key,
+ ctu_id text,
+ coctu_id text,
+ year int,
+ nonres_gro text,
+ nonres_sub text,
+ nonres_typ text,
+ bldg_name text,
+ bldg_desc text,
+ permit_typ text,
+ permit_val numeric,
+ sqf int,
+ address text,
+ zip_code text,
+ pin text,
+ geom geometry (multipoint, 26915)
+);
+
+create index commercial_permit_geom_idx on commercial_permit using gist (
+ geom
+);
+
+insert into commercial_permit (
+ ctu_id,
+ coctu_id,
+ year,
+ nonres_gro,
+ nonres_sub,
+ nonres_typ,
+ bldg_name,
+ bldg_desc,
+ permit_typ,
+ permit_val,
+ sqf,
+ address,
+ zip_code,
+ pin,
+ geom
+)
+select
+ ctu_id,
+ coctu_id,
+ year::int,
+ nonres_gro,
+ nonres_sub,
+ nonres_typ,
+ bldg_name,
+ bldg_desc,
+ permit_typ,
+ permit_val,
+ sqf,
+ address,
+ zip_code,
+ pin,
+ geom
+from
+ commercial_permits_raw
+where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis';
+
+drop table if exists commercial_permit_parcel;
+
+create table commercial_permit_parcel (
+ permit_id int references commercial_permit (id),
+ parcel_id int references parcel (id),
+ type_ region_tag_type
+);
+
+with within as (
+ select
+ commercial_permit.id as permit_id,
+ parcel.id as parcel_id
+ from
+ parcel_with_geom as parcel
+ join commercial_permit on st_within(
+ commercial_permit.geom,
+ parcel.geom
+ )
+ and to_date(
+ year::text,
+ 'YYYY'
+ ) <@ parcel.valid
+),
+not_within as (
+ select
+ id,
+ year,
+ geom
+ from
+ commercial_permit
+ where
+ not exists (
+ select permit_id
+ from
+ within
+ where
+ permit_id = id
+ )
+),
+closest as (
+ select distinct on (permit.id)
+ permit.id as permit_id,
+ parcel.id as parcel_id
+ from
+ not_within as permit
+ join parcel_with_geom as parcel
+ on st_dwithin(permit.geom, parcel.geom, 100.0) and to_date(
+ year::text,
+ 'YYYY'
+ ) <@ parcel.valid
+ order by
+ permit_id,
+ st_distance(
+ permit.geom,
+ parcel.geom
+ )
+)
+insert into commercial_permit_parcel select
+ permit_id,
+ parcel_id,
+ 'within'::region_tag_type
+from
+ within
+union all
+select
+ permit_id,
+ parcel_id,
+ 'closest'::region_tag_type
+from
+ closest;
From 4f9de18ca80eaf4bb621a77613900d01a8a51778 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 7 Aug 2024 09:51:41 -0400
Subject: [PATCH 020/142] create a property values view
---
etl/property_values.sql | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 etl/property_values.sql
diff --git a/etl/property_values.sql b/etl/property_values.sql
new file mode 100644
index 00000000..3e163f9c
--- /dev/null
+++ b/etl/property_values.sql
@@ -0,0 +1,11 @@
+drop view if exists property_values;
+
+create view property_values as (
+ select
+ id
+ , pid
+ , emv_total as value_
+ , valid
+ from
+ parcel);
+
From dba7a29758849c19bc66bfc21c122a58d22b72ea Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 7 Aug 2024 13:44:05 -0400
Subject: [PATCH 021/142] add code to create real_estate_transactions table
---
etl/real_estate_transactions.sql | 72 ++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
create mode 100644 etl/real_estate_transactions.sql
diff --git a/etl/real_estate_transactions.sql b/etl/real_estate_transactions.sql
new file mode 100644
index 00000000..e02ffee8
--- /dev/null
+++ b/etl/real_estate_transactions.sql
@@ -0,0 +1,72 @@
+drop table if exists real_estate_transactions_scraped;
+
+create table real_estate_transactions_scraped (
+ parcel_id text
+ , address text
+ , sale_date date
+ , sale_price numeric
+ , building_area numeric
+ , beds numeric
+ , baths numeric
+ , stories numeric
+ , year_built numeric
+ , neighborhood text
+ , property_type text
+);
+
+\copy real_estate_transactions_scraped from 'zoning/data/processed/real_estate_transactions/real_estate_transactions.csv' with csv header delimiter ',';
+drop table if exists real_estate_transactions_raw;
+
+create table real_estate_transactions_raw (
+ sale_id int
+ , ecrv text
+ , sale_date date
+ , excluded_from_ratio_study text
+ , pin text
+ , num_parcels_in_sale int
+ , formatted_address text
+ , land_sale text
+ , community_cd int
+ , community_desc text
+ , nbhd_cd int
+ , nbhd_desc text
+ , ward int
+ , proptype_cd text
+ , proptype_desc text
+ , grantee1 text
+ , grantee2 text
+ , grantor1 text
+ , grantor2 text
+ , adj_sale_price int
+ , gross_sale_price int
+ , downpayment int
+ , x numeric
+ , y numeric
+ , fid int
+);
+
+\copy real_estate_transactions_raw from 'zoning/data/raw/real_estate_transactions/Property_Sales_2019_to_2023.csv' with csv header delimiter ',';
+drop table if exists real_estate_transactions;
+
+create table real_estate_transactions (
+ id serial primary key
+ , parcel_id int references parcel (id)
+ , address text
+ , sale_date date
+ , sale_price numeric
+ , neighborhood text
+ , property_type text
+);
+
+insert into real_estate_transactions (parcel_id , address , sale_date , sale_price , neighborhood , property_type)
+select
+ parcel.id
+ , address
+ , scraped.sale_date
+ , sale_price
+ , neighborhood
+ , property_type
+from
+ real_estate_transactions_scraped as scraped
+ join parcel on pid = parcel_id
+ and scraped.sale_date <@ valid;
From 6c2d43d1844a55bb639bfce186c310bab0c44df9 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 7 Aug 2024 17:48:13 -0400
Subject: [PATCH 022/142] add code to load acs demographic data
---
etl/acs.sql | 27 ++++++
etl/acs_schema.sql | 50 ++++++++++++
etl/load_acs_raw.py | 195 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 272 insertions(+)
create mode 100644 etl/acs.sql
create mode 100644 etl/acs_schema.sql
create mode 100644 etl/load_acs_raw.py
diff --git a/etl/acs.sql b/etl/acs.sql
new file mode 100644
index 00000000..4026036e
--- /dev/null
+++ b/etl/acs.sql
@@ -0,0 +1,27 @@
+insert into acs_tract
+select
+ id
+ , year_
+ , name_
+ , value_
+from
+ acs_tract_raw as t1
+ join census_tract as t2 on t1.statefp = t2.statefp
+ and t1.countyfp = t2.countyfp
+ and t1.tractce = t2.tractce
+ and to_date(t1.year_::text , 'YYYY') <@ t2.valid;
+
+insert into acs_bg
+select
+ id
+ , year_
+ , name_
+ , value_
+from
+ acs_bg_raw as t1
+ join census_bg as t2 on t1.statefp = t2.statefp
+ and t1.countyfp = t2.countyfp
+ and t1.tractce = t2.tractce
+ and t1.blkgrpce = t2.blkgrpce
+ and to_date(t1.year_::text , 'YYYY') <@ t2.valid;
+
diff --git a/etl/acs_schema.sql b/etl/acs_schema.sql
new file mode 100644
index 00000000..8acb6088
--- /dev/null
+++ b/etl/acs_schema.sql
@@ -0,0 +1,50 @@
+drop table if exists acs_variable cascade;
+
+create table acs_variable (
+ name_ text primary key
+ , description text not null
+);
+
+drop table if exists acs_tract_raw cascade;
+
+create table acs_tract_raw (
+ statefp text
+ , countyfp text
+ , tractce text
+ , year_ int
+ , name_ text
+ , value_ numeric
+);
+
+drop table if exists acs_bg_raw cascade;
+
+create table acs_bg_raw (
+ statefp text
+ , countyfp text
+ , tractce text
+ , blkgrpce text
+ , year_ int
+ , name_ text
+ , value_ numeric
+);
+
+drop table if exists acs_tract cascade;
+
+create table acs_tract (
+ id int references census_tract (id)
+ , year_ int not null
+ , name_ text references acs_variable (name_)
+ , value_ numeric
+ , primary key (id , year_ , name_)
+);
+
+drop table if exists acs_bg cascade;
+
+create table acs_bg (
+ id int references census_bg (id)
+ , year_ int not null
+ , name_ text references acs_variable (name_)
+ , value_ numeric
+ , primary key (id , year_ , name_)
+);
+
diff --git a/etl/load_acs_raw.py b/etl/load_acs_raw.py
new file mode 100644
index 00000000..bc1264be
--- /dev/null
+++ b/etl/load_acs_raw.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python
+
+import logging
+import os
+import psycopg2
+
+from db import HOST, USER
+
+log = logging.getLogger(__name__)
+
+YEAR_RANGE = range(2013, 2023)
+ACS_CODES = {
+ "B03002_003E": "population_white_non_hispanic",
+ "B03002_004E": "population_black_non_hispanic",
+ "B03002_005E": "population_asian_non_hispanic",
+ "B03002_006E": "population_native_hawaiian_or_pacific_islander_non_hispanic",
+ "B03002_007E": "population_american_indian_or_alaska_native_non_hispanic",
+ "B03002_008E": "population_other_non_hispanic",
+ "B03002_009E": "population_multiple_races_non_hispanic",
+ "B03002_010E": "population_multiple_races_and_other_non_hispanic",
+ "B07204_001E": "geographic_mobility_total_responses",
+ "B07204_002E": "geographic_mobility_same_house_1_year_ago",
+ "B07204_004E": "geographic_mobility_different_house_1_year_ago_same_city",
+ "B07204_005E": "geographic_mobility_different_house_1_year_ago_same_county",
+ "B07204_006E": "geographic_mobility_different_house_1_year_ago_same_state",
+ "B07204_007E": "geographic_mobility_different_house_1_year_ago_same_country",
+ "B07204_016E": "geographic_mobility_different_house_1_year_ago_abroad",
+ "B01003_001E": "population",
+ "B02001_002E": "white",
+ "B02001_003E": "black",
+ "B02001_004E": "american_indian_or_alaska_native",
+ "B02001_005E": "asian",
+ "B02001_006E": "native_hawaiian_or_pacific_islander",
+ "B03001_003E": "population_hispanic_or_latino",
+ "B02001_007E": "other_race",
+ "B02001_008E": "multiple_races",
+ "B02001_009E": "multiple_races_and_other_race",
+ "B02001_010E": "two_or_more_races_excluding_other",
+ "B02015_002E": "east_asian_chinese",
+ "B02015_003E": "east_asian_hmong",
+ "B02015_004E": "east_asian_japanese",
+ "B02015_005E": "east_asian_korean",
+ "B02015_006E": "east_asian_mongolian",
+ "B02015_007E": "east_asian_okinawan",
+ "B02015_008E": "east_asian_taiwanese",
+ "B02015_009E": "east_asian_other",
+ "B02015_010E": "southeast_asian_burmese",
+ "B02015_011E": "southeast_asian_cambodian",
+ "B02015_012E": "southeast_asian_filipino",
+ "B02015_013E": "southeast_asian_indonesian",
+ "B02015_014E": "southeast_asian_laotian",
+ "B02015_015E": "southeast_asian_malaysian",
+ "B02015_016E": "southeast_asian_mien",
+ "B02015_017E": "southeast_asian_singaporean",
+ "B02015_018E": "southeast_asian_thai",
+ "B02015_019E": "southeast_asian_viet",
+ "B02015_020E": "southeast_asian_other",
+ "B02015_021E": "south_asian_asian_indian",
+ "B02015_022E": "south_asian_bangladeshi",
+ "B02015_023E": "south_asian_bhutanese",
+ "B02015_024E": "south_asian_nepalese",
+ "B02015_025E": "south_asian_pakistani",
+ "B02015_026E": "south_asian_sikh",
+ "B02015_027E": "south_asian_sri_lankan",
+ "B02015_028E": "south_asian_other",
+ "B02015_029E": "central_asian_kazakh",
+ "B02015_030E": "central_asian_uzbek",
+ "B02015_031E": "central_asian_other",
+ "B02015_032E": "other_asian_specified",
+ "B02015_033E": "other_asian_not_specified",
+ "B19013_001E": "median_household_income",
+ "B19013A_001E": "median_household_income_white",
+ "B19013H_001E": "median_household_income_white_non_hispanic",
+ "B19013I_001E": "median_household_income_hispanic",
+ "B19013B_001E": "median_household_income_black",
+ "B19013C_001E": "median_household_income_american_indian_or_alaska_native",
+ "B19013D_001E": "median_household_income_asian",
+ "B19013E_001E": "median_household_income_native_hawaiian_or_pacific_islander",
+ "B19013F_001E": "median_household_income_other_race",
+ "B19013G_001E": "median_household_income_multiple_races",
+ "B19019_002E": "median_household_income_1_person_households",
+ "B19019_003E": "median_household_income_2_person_households",
+ "B19019_004E": "median_household_income_3_person_households",
+ "B19019_005E": "median_household_income_4_person_households",
+ "B19019_006E": "median_household_income_5_person_households",
+ "B19019_007E": "median_household_income_6_person_households",
+ "B19019_008E": "median_household_income_7_or_more_person_households",
+ "B01002_001E": "median_age",
+ "B01002_002E": "median_age_male",
+ "B01002_003E": "median_age_female",
+ "B25031_001E": "median_gross_rent",
+ "B25031_002E": "median_gross_rent_0_bedrooms",
+ "B25031_003E": "median_gross_rent_1_bedrooms",
+ "B25031_004E": "median_gross_rent_2_bedrooms",
+ "B25031_005E": "median_gross_rent_3_bedrooms",
+ "B25031_006E": "median_gross_rent_4_bedrooms",
+ "B25031_007E": "median_gross_rent_5_bedrooms",
+ "B25032_001E": "total_housing_units",
+ "B25032_002E": "total_owner_occupied_housing_units",
+ "B25032_013E": "total_renter_occupied_housing_units",
+ "B25070_001E": "median_gross_rent_as_percentage_of_household_income",
+}
+
+
+def main():
+ conn = psycopg2.connect(host=HOST, user=USER, database="cities")
+ cur = conn.cursor()
+
+ with open("etl/acs_schema.sql", "r") as f:
+ cur.execute(f.read())
+
+ for code, desc in ACS_CODES.items():
+ cur.execute("insert into acs_variable values (%s, %s)", (code, desc))
+ conn.commit()
+
+ cur.execute("drop table if exists acs_tract_temp")
+ cur.execute(
+ "create temp table acs_tract_temp (statefp text, countyfp text, tractce text, value numeric)"
+ )
+
+ for code in ACS_CODES.keys():
+ desc = ACS_CODES[code]
+ for year in YEAR_RANGE:
+ log.info(f"Loading {desc} for {year}")
+ filename = f"zoning/data/raw/demographics/tracts/{desc}/{year}.csv"
+ if not os.path.isfile(filename):
+ logging.info(f"File {filename} does not exist")
+ continue
+
+ cur.execute("truncate acs_tract_temp")
+
+ with open(filename, "r") as f:
+ cur.copy_expert("copy acs_tract_temp from stdin with csv header", f)
+
+ cur.execute(
+ "insert into acs_tract_raw select statefp, countyfp, tractce, %s, %s, value from acs_tract_temp",
+ (year, code),
+ )
+ # cur.execute(
+ # """
+ # insert into acs_tract
+ # select t2.id, year, name_, value
+ # from (select statefp, countyfp, tractce, %s as year, %s as name_, value from acs_tract_temp) as t1
+ # join census_tract as t2
+ # on to_date(year::varchar, 'YYYY') <@ valid
+ # and t1.statefp = t2.statefp
+ # and t1.countyfp = t2.countyfp
+ # and t1.tractce = t2.tractce
+ # """,
+ # (year, code),
+ # )
+ conn.commit()
+
+ cur.execute("drop table if exists acs_bg_temp")
+ cur.execute(
+ "create temp table acs_bg_temp (statefp text, countyfp text, tractce text, blkgrpce text, value numeric)"
+ )
+
+ for code in ACS_CODES.keys():
+ desc = ACS_CODES[code]
+ for year in YEAR_RANGE:
+ log.info(f"Loading {desc} for {year}")
+ filename = f"zoning/data/raw/demographics/block_groups/{desc}/{year}.csv"
+ if not os.path.isfile(filename):
+ logging.info(f"File {filename} does not exist")
+ continue
+
+ cur.execute("truncate acs_bg_temp")
+
+ with open(filename, "r") as f:
+ cur.copy_expert("copy acs_bg_temp from stdin with csv header", f)
+ cur.execute(
+ "insert into acs_bg_raw select statefp, countyfp, tractce, blkgrpce, %s, %s, value from acs_bg_temp",
+ (year, code),
+ )
+
+ # cur.execute(
+ # """
+ # insert into acs_bg
+ # select t2.id, year, name_, value
+ # from (select statefp, countyfp, tractce, blkgrpce, %s as year, %s as name_, value from acs_bg_temp) as t1
+ # join census_bg as t2
+ # on to_date(year::varchar, 'YYYY') <@ valid
+ # and t1.statefp = t2.statefp
+ # and t1.countyfp = t2.countyfp
+ # and t1.tractce = t2.tractce
+ # """,
+ # (year, code),
+ # )
+ conn.commit()
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ main()
From fef6573828885c43a729185cc6e010616120d1ca Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 7 Aug 2024 17:56:09 -0400
Subject: [PATCH 023/142] remove commented code
---
etl/load_acs_raw.py | 27 ---------------------------
1 file changed, 27 deletions(-)
diff --git a/etl/load_acs_raw.py b/etl/load_acs_raw.py
index bc1264be..4ed52239 100644
--- a/etl/load_acs_raw.py
+++ b/etl/load_acs_raw.py
@@ -136,19 +136,6 @@ def main():
"insert into acs_tract_raw select statefp, countyfp, tractce, %s, %s, value from acs_tract_temp",
(year, code),
)
- # cur.execute(
- # """
- # insert into acs_tract
- # select t2.id, year, name_, value
- # from (select statefp, countyfp, tractce, %s as year, %s as name_, value from acs_tract_temp) as t1
- # join census_tract as t2
- # on to_date(year::varchar, 'YYYY') <@ valid
- # and t1.statefp = t2.statefp
- # and t1.countyfp = t2.countyfp
- # and t1.tractce = t2.tractce
- # """,
- # (year, code),
- # )
conn.commit()
cur.execute("drop table if exists acs_bg_temp")
@@ -173,20 +160,6 @@ def main():
"insert into acs_bg_raw select statefp, countyfp, tractce, blkgrpce, %s, %s, value from acs_bg_temp",
(year, code),
)
-
- # cur.execute(
- # """
- # insert into acs_bg
- # select t2.id, year, name_, value
- # from (select statefp, countyfp, tractce, blkgrpce, %s as year, %s as name_, value from acs_bg_temp) as t1
- # join census_bg as t2
- # on to_date(year::varchar, 'YYYY') <@ valid
- # and t1.statefp = t2.statefp
- # and t1.countyfp = t2.countyfp
- # and t1.tractce = t2.tractce
- # """,
- # (year, code),
- # )
conn.commit()
From 6a05387a16b823a8722107f269534ea1b45f1e31 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 8 Aug 2024 10:03:55 -0400
Subject: [PATCH 024/142] add code to load fair market rents
---
etl/fair_market_rents.sql | 63 +++++++++++++++++++++
etl/fair_market_rents_schema.sql | 12 ++++
etl/load_fair_market_rents_raw.py | 94 +++++++++++++++++++++++++++++++
3 files changed, 169 insertions(+)
create mode 100644 etl/fair_market_rents.sql
create mode 100644 etl/fair_market_rents_schema.sql
create mode 100644 etl/load_fair_market_rents_raw.py
diff --git a/etl/fair_market_rents.sql b/etl/fair_market_rents.sql
new file mode 100644
index 00000000..d2eb3137
--- /dev/null
+++ b/etl/fair_market_rents.sql
@@ -0,0 +1,63 @@
+drop table if exists fair_market_rents cascade;
+
+create table fair_market_rents (
+ zip_id int references zip_code (id)
+ , rent numeric
+ , num_bedrooms int
+ , year_ int
+);
+
+insert into fair_market_rents (zip_id , rent , num_bedrooms , year_)
+with fmr_zip as (
+ select
+ zip_code.id as zip_id
+ , rent_br0
+ , rent_br1
+ , rent_br2
+ , rent_br3
+ , rent_br4
+ , year_
+ from
+ fair_market_rents_raw
+ join zip_code on zip_code.zip_code = fair_market_rents_raw.zip
+ and zip_code.valid @> to_date(year_::text , 'YYYY'))
+ select
+ zip_id
+ , rent_br0
+ , 0
+ , year_
+ from
+ fmr_zip
+ union
+ select
+ zip_id
+ , rent_br1
+ , 1
+ , year_
+ from
+ fmr_zip
+ union
+ select
+ zip_id
+ , rent_br2
+ , 2
+ , year_
+ from
+ fmr_zip
+ union
+ select
+ zip_id
+ , rent_br3
+ , 3
+ , year_
+ from
+ fmr_zip
+ union
+ select
+ zip_id
+ , rent_br4
+ , 4
+ , year_
+ from
+ fmr_zip;
+
diff --git a/etl/fair_market_rents_schema.sql b/etl/fair_market_rents_schema.sql
new file mode 100644
index 00000000..4fd2ac52
--- /dev/null
+++ b/etl/fair_market_rents_schema.sql
@@ -0,0 +1,12 @@
+drop table if exists fair_market_rents_raw cascade;
+
+create table fair_market_rents_raw (
+ zip text
+ , rent_br0 numeric
+ , rent_br1 numeric
+ , rent_br2 numeric
+ , rent_br3 numeric
+ , rent_br4 numeric
+ , year_ int
+);
+
diff --git a/etl/load_fair_market_rents_raw.py b/etl/load_fair_market_rents_raw.py
new file mode 100644
index 00000000..565c8a05
--- /dev/null
+++ b/etl/load_fair_market_rents_raw.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+
+import logging
+import os
+import glob
+from io import StringIO
+
+import psycopg2
+import pandas as pd
+
+from db import HOST, USER
+
+log = logging.getLogger(__name__)
+
+RAW_DATA_DIRECTORY = "zoning/data/raw/demographics/zip_codes/fair_market_rents"
+
+
+def preprocess_csv_to_df(filename):
+ year = filename.split("_")[-1].replace(".csv", "")
+
+ df = pd.read_csv(filename, dtype=str, na_values={})
+
+ rename_dict = {}
+ for col in list(df.columns):
+ if "zip" in col.lower() or col == "zcta":
+ rename_dict[col] = "zip_code"
+ elif "BR" in col and "90" not in col and "110" not in col:
+ rename_dict[col] = "rent_br" + col.lower().split("br")[0][-1]
+ elif "area_rent_br" in col:
+ rename_dict[col] = "rent_br" + col[-1]
+ elif "safmr" in col and "90" not in col and "110" not in col:
+ rename_dict[col] = "rent_br" + col.split("_")[-1][0]
+
+ df = df.rename(columns=rename_dict)[
+ [
+ "zip_code",
+ "rent_br0",
+ "rent_br1",
+ "rent_br2",
+ "rent_br3",
+ "rent_br4",
+ ]
+ ]
+
+ for col in df.columns:
+ if "rent_" in col:
+ df[col] = [x.replace("$", "").replace(",", "") for x in df[col]]
+
+ return (year, df)
+
+
+def copy_from_stringio(cur, df, table):
+ """Here we are going save the dataframe in memory and use copy_from() to copy it to the table"""
+ buf = StringIO()
+ df.to_csv(buf, index=False, header=False)
+ buf.seek(0)
+ cur.copy_from(buf, table, sep=",")
+
+
+def main():
+ conn = psycopg2.connect(host=HOST, user=USER, database="cities")
+ cur = conn.cursor()
+
+ with open("etl/fair_market_rents_schema.sql", "r") as f:
+ cur.execute(f.read())
+
+ cur.execute("drop table if exists fmr_temp")
+ cur.execute(
+ """
+ create temp table fmr_temp (
+ zip text
+ , rent_br0 numeric
+ , rent_br1 numeric
+ , rent_br2 numeric
+ , rent_br3 numeric
+ , rent_br4 numeric)
+ """
+ )
+
+ for filename in glob.glob(f"{RAW_DATA_DIRECTORY}/*.csv"):
+ (year, df) = preprocess_csv_to_df(filename)
+ cur.execute("truncate fmr_temp")
+ copy_from_stringio(cur, df, "fmr_temp")
+
+ cur.execute(
+ "insert into fair_market_rents_raw select *, %s as year from fmr_temp",
+ (year,),
+ )
+ conn.commit()
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ main()
From dc6acb76d1d7045d7f734fa125cc237d2719650e Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 8 Aug 2024 10:53:54 -0400
Subject: [PATCH 025/142] add code to load usps migration data
---
etl/load_usps_migration_raw.py | 64 ++++++++++++
etl/usps_migration.sql | 157 ++++++++++++++++++++++++++++++
etl/usps_migration_raw_schema.sql | 22 +++++
3 files changed, 243 insertions(+)
create mode 100644 etl/load_usps_migration_raw.py
create mode 100644 etl/usps_migration.sql
create mode 100644 etl/usps_migration_raw_schema.sql
diff --git a/etl/load_usps_migration_raw.py b/etl/load_usps_migration_raw.py
new file mode 100644
index 00000000..c05f8e0b
--- /dev/null
+++ b/etl/load_usps_migration_raw.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+
+import glob
+import logging
+import psycopg2
+
+from db import HOST, USER
+
+log = logging.getLogger(__name__)
+
+
+RAW_DATA_DIRECTORY = "zoning/data/raw/demographics/zip_codes/usps_migration"
+
+
+def main():
+ conn = psycopg2.connect(host=HOST, user=USER, database="cities")
+ cur = conn.cursor()
+
+ with open("etl/usps_migration_raw_schema.sql", "r") as f:
+ cur.execute(f.read())
+
+ cur.execute("drop table if exists m_temp")
+ cur.execute(
+ """
+ create temp table m_temp (
+ yyyymm text
+ , zip_code text
+ , city text
+ , state text
+ , total_from_zip numeric
+ , total_from_zip_business numeric
+ , total_from_zip_family numeric
+ , total_from_zip_individual numeric
+ , total_from_zip_perm numeric
+ , total_from_zip_temp numeric
+ , total_to_zip numeric
+ , total_to_zip_business numeric
+ , total_to_zip_family numeric
+ , total_to_zip_individual numeric
+ , total_to_zip_perm numeric
+ , total_to_zip_temp numeric
+ )
+ """
+ )
+
+ for filename in glob.glob(f"{RAW_DATA_DIRECTORY}/*.csv"):
+ log.info(f"Loading {filename}")
+ year = filename.split("/")[-1].split(".")[0].replace("Y", "")
+
+ cur.execute("truncate m_temp")
+
+ with open(filename, "r") as f:
+ cur.copy_expert("copy m_temp from stdin with csv header", f)
+
+ cur.execute(
+ "insert into usps_migration_raw select *, %s from m_temp",
+ (year,),
+ )
+ conn.commit()
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ main()
diff --git a/etl/usps_migration.sql b/etl/usps_migration.sql
new file mode 100644
index 00000000..0f6394de
--- /dev/null
+++ b/etl/usps_migration.sql
@@ -0,0 +1,157 @@
+drop type if exists usps_migration_flow_direction cascade;
+
+create type usps_migration_flow_direction as enum (
+ 'in'
+ , 'out'
+);
+
+drop enum if exists usps_migration_flow_type cascade;
+
+create type usps_migration_flow_type as enum (
+ 'total'
+ , 'business'
+ , 'family'
+ , 'individual'
+ , 'perm'
+ , 'temp'
+);
+
+drop table if exists usps_migration cascade;
+
+create table usps_migration (
+ date_ date not null check (extract(day from date_) = 1) -- granularity is year-month
+ , zip_id int references zip_code (id)
+ , direction usps_migration_flow_direction not null
+ , type_ usps_migration_flow_type not null
+ , flow numeric
+ , primary key (date_ , zip_id , direction , type_)
+);
+
+-- explain insert into usps_migration (date_, zip_id, direction, type_, flow)
+insert into usps_migration with process_date as (
+ select
+ to_date(yyyymm
+ , 'YYYYMM') as date_
+ , *
+ from
+ usps_migration_raw
+)
+, add_zip_id as (
+ select
+ zip_code.id as zip_id
+ , mr.*
+ from
+ process_date as mr
+ join zip_code on zip_code.zip_code = replace(mr.zip_code
+ , '='
+ , '')
+ and zip_code.valid @> to_date(year_::text
+ , 'YYYY'))
+ select
+ date_
+ , zip_id
+ , 'in'::usps_migration_flow_direction
+ , 'total'::usps_migration_flow_type
+ , total_from_zip
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'in'::usps_migration_flow_direction
+ , 'business'::usps_migration_flow_type
+ , total_from_zip_business
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'in'::usps_migration_flow_direction
+ , 'family'::usps_migration_flow_type
+ , total_from_zip_family
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'in'::usps_migration_flow_direction
+ , 'individual'::usps_migration_flow_type
+ , total_from_zip_individual
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'in'::usps_migration_flow_direction
+ , 'perm'::usps_migration_flow_type
+ , total_from_zip_perm
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'in'::usps_migration_flow_direction
+ , 'temp'::usps_migration_flow_type
+ , total_from_zip_temp
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'out'::usps_migration_flow_direction
+ , 'total'::usps_migration_flow_type
+ , total_to_zip
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'out'::usps_migration_flow_direction
+ , 'business'::usps_migration_flow_type
+ , total_to_zip_business
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'out'::usps_migration_flow_direction
+ , 'family'::usps_migration_flow_type
+ , total_to_zip_family
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'out'::usps_migration_flow_direction
+ , 'individual'::usps_migration_flow_type
+ , total_to_zip_individual
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'out'::usps_migration_flow_direction
+ , 'perm'::usps_migration_flow_type
+ , total_to_zip_perm
+ from
+ add_zip_id
+ union all
+ select
+ date_
+ , zip_id
+ , 'out'::usps_migration_flow_direction
+ , 'temp'::usps_migration_flow_type
+ , total_to_zip_temp
+ from
+ add_zip_id;
+
diff --git a/etl/usps_migration_raw_schema.sql b/etl/usps_migration_raw_schema.sql
new file mode 100644
index 00000000..50a823ff
--- /dev/null
+++ b/etl/usps_migration_raw_schema.sql
@@ -0,0 +1,22 @@
+drop table if exists usps_migration_raw cascade;
+
+create table usps_migration_raw (
+ yyyymm text
+ , zip_code text
+ , city text
+ , state text
+ , total_from_zip numeric
+ , total_from_zip_business numeric
+ , total_from_zip_family numeric
+ , total_from_zip_individual numeric
+ , total_from_zip_perm numeric
+ , total_from_zip_temp numeric
+ , total_to_zip numeric
+ , total_to_zip_business numeric
+ , total_to_zip_family numeric
+ , total_to_zip_individual numeric
+ , total_to_zip_perm numeric
+ , total_to_zip_temp numeric
+ , year_ int
+);
+
From f868a53f379f17a6155cef58ed49fa06ae38612a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 8 Aug 2024 10:59:45 -0400
Subject: [PATCH 026/142] remove comment
---
etl/usps_migration.sql | 2 --
1 file changed, 2 deletions(-)
diff --git a/etl/usps_migration.sql b/etl/usps_migration.sql
index 0f6394de..df30498c 100644
--- a/etl/usps_migration.sql
+++ b/etl/usps_migration.sql
@@ -27,7 +27,6 @@ create table usps_migration (
, primary key (date_ , zip_id , direction , type_)
);
--- explain insert into usps_migration (date_, zip_id, direction, type_, flow)
insert into usps_migration with process_date as (
select
to_date(yyyymm
@@ -154,4 +153,3 @@ insert into usps_migration with process_date as (
, total_to_zip_temp
from
add_zip_id;
-
From 04b83cdc17cc1409b27c293474ff4175afdf7237 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 8 Aug 2024 11:18:43 -0400
Subject: [PATCH 027/142] rename schema.sql to parcel_schema.sql
---
etl/load_parcels.py | 2 +-
etl/{schema.sql => parcel_schema.sql} | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
rename etl/{schema.sql => parcel_schema.sql} (99%)
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
index 6604281f..ff2d5c67 100644
--- a/etl/load_parcels.py
+++ b/etl/load_parcels.py
@@ -10,7 +10,7 @@
conn = psycopg2.connect(host=HOST, user=USER, database="cities")
cur = conn.cursor()
-with open("etl/schema.sql", "r") as f:
+with open("etl/parcel_schema.sql", "r") as f:
cur.execute(f.read())
conn.commit()
diff --git a/etl/schema.sql b/etl/parcel_schema.sql
similarity index 99%
rename from etl/schema.sql
rename to etl/parcel_schema.sql
index 1029c451..24bf8523 100644
--- a/etl/schema.sql
+++ b/etl/parcel_schema.sql
@@ -33,3 +33,4 @@ comment on column parcel.emv_land is 'Estimated Market Value, land';
comment on column parcel.emv_building is 'Estimated Market Value, buildings';
comment on column parcel.emv_total is 'Estimated Market Value, total (may be more than sum of land and building)';
+
From d3fdee842d4cc52f0e95c4647be8e817ffd4fa55 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 8 Aug 2024 11:20:07 -0400
Subject: [PATCH 028/142] clean up load_parcels.py
---
etl/load_parcels.py | 80 +++++++++++++++++++++++++--------------------
1 file changed, 45 insertions(+), 35 deletions(-)
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
index ff2d5c67..7a114b34 100644
--- a/etl/load_parcels.py
+++ b/etl/load_parcels.py
@@ -1,44 +1,54 @@
#!/usr/bin/env python
+import logging
import psycopg2
from db import HOST, USER
+log = logging.getLogger(__name__)
+
PARCEL_YEARS = range(2002, 2024)
COUNTY_ID = "053"
-conn = psycopg2.connect(host=HOST, user=USER, database="cities")
-cur = conn.cursor()
-
-with open("etl/parcel_schema.sql", "r") as f:
- cur.execute(f.read())
-conn.commit()
-
-# select distinct geometry from all parcel tables
-distinct_geom = " union ".join(
- f"select geom from parcel_raw_{year} where upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'"
- for year in PARCEL_YEARS
-)
-parcel_geom_load = f"insert into parcel_geom (geom) {distinct_geom};"
-print("Executing:", parcel_geom_load)
-cur.execute(parcel_geom_load)
-conn.commit()
-
-# insert parcel data into parcel table
-parcel_data = " union all ".join(
- f"""
- select replace(pin, '{COUNTY_ID}-', ''), '[{year-1}-01-01,{year}-01-01)'::daterange, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
- from parcel_raw_{year}, parcel_geom
- where parcel_raw_{year}.geom = parcel_geom.geom
- and upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'
- """
- for year in PARCEL_YEARS
-)
-
-parcel_load = f"""
-insert into parcel (pid, valid, emv_land, emv_building, emv_total, year_built, sale_date, sale_value, geom_id)
- {parcel_data}
- """
-print("Executing:", parcel_load)
-cur.execute(parcel_load)
-conn.commit()
+
+def main():
+ conn = psycopg2.connect(host=HOST, user=USER, database="cities")
+ cur = conn.cursor()
+
+ with open("etl/parcel_schema.sql", "r") as f:
+ cur.execute(f.read())
+ conn.commit()
+
+ # select distinct geometry from all parcel tables
+ distinct_geom = " union ".join(
+ f"select geom from parcel_raw_{year} where upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'"
+ for year in PARCEL_YEARS
+ )
+ parcel_geom_load = f"insert into parcel_geom (geom) {distinct_geom};"
+ log.info("Executing: %s", parcel_geom_load)
+ cur.execute(parcel_geom_load)
+ conn.commit()
+
+ # insert parcel data into parcel table
+ parcel_data = " union all ".join(
+ f"""
+ select replace(pin, '{COUNTY_ID}-', ''), '[{year-1}-01-01,{year}-01-01)'::daterange, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
+ from parcel_raw_{year}, parcel_geom
+ where parcel_raw_{year}.geom = parcel_geom.geom
+ and upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'
+ """
+ for year in PARCEL_YEARS
+ )
+
+ parcel_load = f"""
+ insert into parcel (pid, valid, emv_land, emv_building, emv_total, year_built, sale_date, sale_value, geom_id)
+ {parcel_data}
+ """
+ log.info("Executing: %s", parcel_load)
+ cur.execute(parcel_load)
+ conn.commit()
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ main()
From 14a297b08919db2ef14a331a4ef8d04af41ccb8b Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 8 Aug 2024 11:53:49 -0400
Subject: [PATCH 029/142] reformat
---
etl/census_schema.sql | 1 +
etl/parcel_to_bg.sql | 1 +
etl/permit_schema.sql | 384 +++++++++++++------------------
etl/real_estate_transactions.sql | 1 +
etl/usps_migration.sql | 1 +
etl/zip_schema.sql | 16 +-
6 files changed, 184 insertions(+), 220 deletions(-)
diff --git a/etl/census_schema.sql b/etl/census_schema.sql
index 521bd20b..b90b5a90 100644
--- a/etl/census_schema.sql
+++ b/etl/census_schema.sql
@@ -88,3 +88,4 @@ from (
join census_tract using (statefp , countyfp , tractce)
where
census_tract.valid && bg.valid;
+
diff --git a/etl/parcel_to_bg.sql b/etl/parcel_to_bg.sql
index ebee0dde..5dbc7ac9 100644
--- a/etl/parcel_to_bg.sql
+++ b/etl/parcel_to_bg.sql
@@ -115,3 +115,4 @@ with parcel_with_geom as (
, 'closest'::parcel_census_bg_type
from
parcel_closest;
+
diff --git a/etl/permit_schema.sql b/etl/permit_schema.sql
index e1dc5df2..c5c7e5e9 100644
--- a/etl/permit_schema.sql
+++ b/etl/permit_schema.sql
@@ -1,82 +1,57 @@
drop table if exists residential_permit cascade;
create table residential_permit (
- id serial primary key,
- ctu_id text,
- coctu_id text,
- year int,
- tenure text,
- housing_ty text,
- res_permit text,
- address text,
- zip_code text,
- name text,
- buildings int,
- units int,
- age_restri int,
- memory_car int,
- assisted int,
- com_off_re boolean,
- sqf numeric,
- public_fun boolean,
- permit_val numeric,
- community_ text,
- notes text,
- pin text,
- geom geometry (multipoint, 26915)
+ id serial primary key
+ , ctu_id text
+ , coctu_id text
+ , year int
+ , tenure text
+ , housing_ty text
+ , res_permit text
+ , address text
+ , zip_code text
+ , name text
+ , buildings int
+ , units int
+ , age_restri int
+ , memory_car int
+ , assisted int
+ , com_off_re boolean
+ , sqf numeric
+ , public_fun boolean
+ , permit_val numeric
+ , community_ text
+ , notes text
+ , pin text
+ , geom geometry(multipoint , 26915)
);
-create index residential_permit_geom_idx on residential_permit using gist (
- geom
-);
+create index residential_permit_geom_idx on residential_permit using gist (geom);
-insert into residential_permit (
- ctu_id,
- coctu_id,
- year,
- tenure,
- housing_ty,
- res_permit,
- address,
- zip_code,
- name,
- buildings,
- units,
- age_restri,
- memory_car,
- assisted,
- com_off_re,
- sqf,
- public_fun,
- permit_val,
- community_,
- notes,
- pin,
- geom
-)
+insert into residential_permit (ctu_id , coctu_id , year , tenure , housing_ty , res_permit , address , zip_code , name , buildings , units , age_restri , memory_car , assisted , com_off_re , sqf , public_fun , permit_val , community_ , notes , pin , geom)
select
- ctu_id,
- coctu_id,
- year::int,
- tenure,
- housing_ty,
- res_permit,
- address,
- zip_code,
- name,
- buildings,
- units,
- age_restri,
- memory_car,
- assisted,
- com_off_re = 'Y',
- sqf,
- public_fun = 'Y',
- permit_val,
- community_,
- notes,
- pin,
- geom
+ ctu_id
+ , coctu_id
+ , year::int
+ , tenure
+ , housing_ty
+ , res_permit
+ , address
+ , zip_code
+ , name
+ , buildings
+ , units
+ , age_restri
+ , memory_car
+ , assisted
+ , com_off_re = 'Y'
+ , sqf
+ , public_fun = 'Y'
+ , permit_val
+ , community_
+ , notes
+ , pin
+ , geom
from
residential_permits_raw
where
@@ -86,132 +61,108 @@ where
drop table if exists residential_permit_parcel;
create table residential_permit_parcel (
- permit_id int references residential_permit (id),
- parcel_id int references parcel (id),
- type_ region_tag_type
+ permit_id int references residential_permit (id)
+ , parcel_id int references parcel (id)
+ , type_ region_tag_type
);
with within as (
select
- residential_permit.id as permit_id,
- parcel.id as parcel_id
+ residential_permit.id as permit_id
+ , parcel.id as parcel_id
from
parcel_with_geom as parcel
- join residential_permit on st_within(
- residential_permit.geom,
- parcel.geom
- )
- and to_date(
- year::text,
- 'YYYY'
- ) <@ parcel.valid
-),
-not_within as (
+ join residential_permit on st_within (residential_permit.geom
+ , parcel.geom)
+ and to_date(year::text
+ , 'YYYY') <@ parcel.valid
+)
+, not_within as (
select
- id,
- year,
- geom
+ id
+ , year
+ , geom
from
residential_permit
where
not exists (
- select permit_id
+ select
+ permit_id
from
within
where
- permit_id = id
- )
-),
-closest as (
+ permit_id = id)
+)
+, closest as (
select distinct on (permit.id)
- permit.id as permit_id,
- parcel.id as parcel_id
+ permit.id as permit_id
+ , parcel.id as parcel_id
from
not_within as permit
- join parcel_with_geom as parcel
- on st_dwithin(permit.geom, parcel.geom, 100.0) and to_date(
- year::text,
- 'YYYY'
- ) <@ parcel.valid
- order by
- permit_id,
- st_distance(
- permit.geom,
- parcel.geom
- )
-)
-insert into residential_permit_parcel select
- permit_id,
- parcel_id,
- 'within'::region_tag_type
-from
- within
-union all
-select
- permit_id,
- parcel_id,
- 'closest'::region_tag_type
+ join parcel_with_geom as parcel on st_dwithin (permit.geom
+ , parcel.geom
+ , 100.0)
+ and to_date(year::text
+ , 'YYYY') <@ parcel.valid
+ order by
+ permit_id
+ , st_distance (permit.geom
+ , parcel.geom))
+ insert into residential_permit_parcel
+ select
+ permit_id
+ , parcel_id
+ , 'within'::region_tag_type
+ from
+ within
+ union all
+ select
+ permit_id
+ , parcel_id
+ , 'closest'::region_tag_type
from
closest;
drop table if exists commercial_permit cascade;
create table commercial_permit (
- id serial primary key,
- ctu_id text,
- coctu_id text,
- year int,
- nonres_gro text,
- nonres_sub text,
- nonres_typ text,
- bldg_name text,
- bldg_desc text,
- permit_typ text,
- permit_val numeric,
- sqf int,
- address text,
- zip_code text,
- pin text,
- geom geometry (multipoint, 26915)
+ id serial primary key
+ , ctu_id text
+ , coctu_id text
+ , year int
+ , nonres_gro text
+ , nonres_sub text
+ , nonres_typ text
+ , bldg_name text
+ , bldg_desc text
+ , permit_typ text
+ , permit_val numeric
+ , sqf int
+ , address text
+ , zip_code text
+ , pin text
+ , geom geometry(multipoint , 26915)
);
-create index commercial_permit_geom_idx on commercial_permit using gist (
- geom
-);
+create index commercial_permit_geom_idx on commercial_permit using gist (geom);
-insert into commercial_permit (
- ctu_id,
- coctu_id,
- year,
- nonres_gro,
- nonres_sub,
- nonres_typ,
- bldg_name,
- bldg_desc,
- permit_typ,
- permit_val,
- sqf,
- address,
- zip_code,
- pin,
- geom
-)
+insert into commercial_permit (ctu_id , coctu_id , year , nonres_gro , nonres_sub , nonres_typ , bldg_name , bldg_desc , permit_typ , permit_val , sqf , address , zip_code , pin , geom)
select
- ctu_id,
- coctu_id,
- year::int,
- nonres_gro,
- nonres_sub,
- nonres_typ,
- bldg_name,
- bldg_desc,
- permit_typ,
- permit_val,
- sqf,
- address,
- zip_code,
- pin,
- geom
+ ctu_id
+ , coctu_id
+ , year::int
+ , nonres_gro
+ , nonres_sub
+ , nonres_typ
+ , bldg_name
+ , bldg_desc
+ , permit_typ
+ , permit_val
+ , sqf
+ , address
+ , zip_code
+ , pin
+ , geom
from
commercial_permits_raw
where
@@ -221,70 +172,65 @@ where
drop table if exists commercial_permit_parcel;
create table commercial_permit_parcel (
- permit_id int references commercial_permit (id),
- parcel_id int references parcel (id),
- type_ region_tag_type
+ permit_id int references commercial_permit (id)
+ , parcel_id int references parcel (id)
+ , type_ region_tag_type
);
with within as (
select
- commercial_permit.id as permit_id,
- parcel.id as parcel_id
+ commercial_permit.id as permit_id
+ , parcel.id as parcel_id
from
parcel_with_geom as parcel
- join commercial_permit on st_within(
- commercial_permit.geom,
- parcel.geom
- )
- and to_date(
- year::text,
- 'YYYY'
- ) <@ parcel.valid
-),
-not_within as (
+ join commercial_permit on st_within (commercial_permit.geom
+ , parcel.geom)
+ and to_date(year::text
+ , 'YYYY') <@ parcel.valid
+)
+, not_within as (
select
- id,
- year,
- geom
+ id
+ , year
+ , geom
from
commercial_permit
where
not exists (
- select permit_id
+ select
+ permit_id
from
within
where
- permit_id = id
- )
-),
-closest as (
+ permit_id = id)
+)
+, closest as (
select distinct on (permit.id)
- permit.id as permit_id,
- parcel.id as parcel_id
+ permit.id as permit_id
+ , parcel.id as parcel_id
from
not_within as permit
- join parcel_with_geom as parcel
- on st_dwithin(permit.geom, parcel.geom, 100.0) and to_date(
- year::text,
- 'YYYY'
- ) <@ parcel.valid
- order by
- permit_id,
- st_distance(
- permit.geom,
- parcel.geom
- )
-)
-insert into commercial_permit_parcel select
- permit_id,
- parcel_id,
- 'within'::region_tag_type
-from
- within
-union all
-select
- permit_id,
- parcel_id,
- 'closest'::region_tag_type
+ join parcel_with_geom as parcel on st_dwithin (permit.geom
+ , parcel.geom
+ , 100.0)
+ and to_date(year::text
+ , 'YYYY') <@ parcel.valid
+ order by
+ permit_id
+ , st_distance (permit.geom
+ , parcel.geom))
+ insert into commercial_permit_parcel
+ select
+ permit_id
+ , parcel_id
+ , 'within'::region_tag_type
+ from
+ within
+ union all
+ select
+ permit_id
+ , parcel_id
+ , 'closest'::region_tag_type
from
closest;
+
diff --git a/etl/real_estate_transactions.sql b/etl/real_estate_transactions.sql
index e02ffee8..6980b5ed 100644
--- a/etl/real_estate_transactions.sql
+++ b/etl/real_estate_transactions.sql
@@ -70,3 +70,4 @@ from
real_estate_transactions_scraped as scraped
join parcel on pid = parcel_id
and scraped.sale_date <@ valid;
+
diff --git a/etl/usps_migration.sql b/etl/usps_migration.sql
index df30498c..9a123bb4 100644
--- a/etl/usps_migration.sql
+++ b/etl/usps_migration.sql
@@ -153,3 +153,4 @@ insert into usps_migration with process_date as (
, total_to_zip_temp
from
add_zip_id;
+
diff --git a/etl/zip_schema.sql b/etl/zip_schema.sql
index d1a93ab7..1d9ab6c8 100644
--- a/etl/zip_schema.sql
+++ b/etl/zip_schema.sql
@@ -1,4 +1,4 @@
-drop table if exists zip_code;
+drop table if exists zip_code cascade;
create table zip_code (
id serial primary key
@@ -9,3 +9,17 @@ create table zip_code (
create index zip_code_geom_idx on zip_code using gist (geom);
+insert into zip_code (zip_code , valid , geom)
+select
+ zcta5ce20
+ , '[2020-01-01,)'::daterange
+ , geom
+from
+ zip_raw_2020
+union
+select
+ zcta
+ , '[2000-01-01,2020-01-01)'::daterange
+ , ST_Transform (geom , 4269)
+from
+ zip_raw_2000
From 520efed06a2b0f42e72b8a1254fb6283e8d9724f Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 9 Aug 2024 14:13:25 -0400
Subject: [PATCH 030/142] add segregation index
---
etl/segregation.sql | 92 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 92 insertions(+)
create mode 100644 etl/segregation.sql
diff --git a/etl/segregation.sql b/etl/segregation.sql
new file mode 100644
index 00000000..ba25e911
--- /dev/null
+++ b/etl/segregation.sql
@@ -0,0 +1,92 @@
+create or replace view categories as select * from (
+ values
+ ('population_white_non_hispanic'),
+ ('population_black_non_hispanic'),
+ ('population_hispanic_or_latino'),
+ ('population_asian_non_hispanic'),
+ ('population_native_hawaiian_or_pacific_islander_non_hispanic'),
+ ('population_american_indian_or_alaska_native_non_hispanic'),
+ ('population_multiple_races_non_hispanic'),
+ ('population_other_non_hispanic')
+) as t (description);
+
+drop type if exists reference_distribution cascade;
+create type reference_distribution as enum (
+ 'uniform'
+ , 'annual_city'
+ , 'average_city'
+);
+
+
+-- Segregation index for each tract for each year, computed for each reference
+-- distribution.
+--
+-- The segregation index is the KL-divergence between the distribution of
+-- population in a tract and a reference distribution. For example, a tract that
+-- has many more white people than the average for the city will have a high
+-- segregation index for the 'average_city' distribution.
+
+drop table if exists segregation;
+
+create table segregation as (
+with
+ pop_tyc as
+ ( -- Population by tract, year, and category
+ select id, year_, description, value_
+ from acs_tract
+ join acs_variable using (name_)
+ join categories using (description)
+ ),
+ pop_ty as
+ ( -- Population by tract and year (note: using 'population' variable instead of aggregating categories)
+ select id, year_, value_
+ from acs_tract join acs_variable using (name_)
+ where description = 'population'
+ ),
+ pop_yc as
+ ( -- Population by year and category
+ select year_, description, sum(value_) as value_
+ from pop_tyc group by year_, description
+ ),
+ pop_y as
+ ( -- Population by year
+ select year_, sum(value_) as value_ from pop_ty group by year_
+ ),
+ dist_yc as
+ ( -- Distribution of population by year and category
+ select description, c.year_,
+ case t.value_ when 0 then 0 else c.value_ / t.value_ end as value_
+ from pop_yc as c join pop_y as t using (year_)
+ ),
+ dist_tyc as
+ ( -- Distribution of population by tract, year, and category
+ select id, year_, description,
+ case t.value_ when 0 then 0 else p.value_ / t.value_ end as value_
+ from pop_tyc as p join pop_ty as t using (year_, id)
+ ),
+ uniform_dist as
+ ( -- Uniform distribution across categories
+ with n_cat as (select count(*) as n_cat from categories)
+ select description, 1.0 / n_cat as value_
+ from categories, n_cat
+ ),
+ average_dist as
+ ( -- Average of the annual citywide distributions
+ select description, avg(value_) as value_
+ from dist_yc
+ group by description
+ )
+select id, year_, dist, sum(case when p = 0 or q = 0 then 0 else p * ln(p / q) end) as segregation_index
+ from
+ (
+ select id, year_, 'uniform'::reference_distribution as dist, dist_tyc.value_ as p, uniform_dist.value_ as q
+ from dist_tyc join uniform_dist using (description)
+ union all
+ select id, year_, 'annual_city'::reference_distribution as dist, dist_tyc.value_ as p, dist_yc.value_ as q
+ from dist_tyc join dist_yc using (year_, description)
+ union all
+ select id, year_, 'average_city'::reference_distribution as dist, dist_tyc.value_ as p, average_dist.value_ as q
+ from dist_tyc join average_dist using (description)
+ )
+ group by id, year_, dist
+);
From 12c528971ab8a23d7d3f3670bcb94a5ce4846d61 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 13 Aug 2024 17:44:58 -0400
Subject: [PATCH 031/142] add dbt version of transformations
---
dbt/.gitignore | 3 +
dbt/README.md | 15 +++++
dbt/analyses/.gitkeep | 0
dbt/dbt_project.yml | 28 ++++++++
dbt/macros/.gitkeep | 0
dbt/macros/tag_regions.sql | 67 +++++++++++++++++++
dbt/models/acs_block_group.sql | 23 +++++++
dbt/models/acs_tract.sql | 23 +++++++
dbt/models/census_block_groups.sql | 53 +++++++++++++++
dbt/models/census_tracts.sql | 26 +++++++
dbt/models/commercial_permits.sql | 13 ++++
dbt/models/commercial_permits_base.sql | 18 +++++
dbt/models/commercial_permits_to_parcels.sql | 21 ++++++
dbt/models/fair_market_rents.sql | 32 +++++++++
dbt/models/parcels.sql | 21 ++++++
dbt/models/parcels_base.sql | 33 +++++++++
dbt/models/parcels_to_census_block_groups.sql | 21 ++++++
dbt/models/parcels_to_zip_codes.sql | 21 ++++++
dbt/models/residential_permits.sql | 13 ++++
dbt/models/residential_permits_base.sql | 25 +++++++
dbt/models/residential_permits_to_parcels.sql | 21 ++++++
dbt/models/usps_migration.sql | 44 ++++++++++++
dbt/models/zip_codes.sql | 20 ++++++
dbt/package-lock.yml | 4 ++
dbt/packages.yml | 3 +
dbt/seeds/.gitkeep | 0
dbt/seeds/region_tag_type.csv | 4 ++
dbt/snapshots/.gitkeep | 0
dbt/tests/.gitkeep | 0
29 files changed, 552 insertions(+)
create mode 100644 dbt/.gitignore
create mode 100644 dbt/README.md
create mode 100644 dbt/analyses/.gitkeep
create mode 100644 dbt/dbt_project.yml
create mode 100644 dbt/macros/.gitkeep
create mode 100644 dbt/macros/tag_regions.sql
create mode 100644 dbt/models/acs_block_group.sql
create mode 100644 dbt/models/acs_tract.sql
create mode 100644 dbt/models/census_block_groups.sql
create mode 100644 dbt/models/census_tracts.sql
create mode 100644 dbt/models/commercial_permits.sql
create mode 100644 dbt/models/commercial_permits_base.sql
create mode 100644 dbt/models/commercial_permits_to_parcels.sql
create mode 100644 dbt/models/fair_market_rents.sql
create mode 100644 dbt/models/parcels.sql
create mode 100644 dbt/models/parcels_base.sql
create mode 100644 dbt/models/parcels_to_census_block_groups.sql
create mode 100644 dbt/models/parcels_to_zip_codes.sql
create mode 100644 dbt/models/residential_permits.sql
create mode 100644 dbt/models/residential_permits_base.sql
create mode 100644 dbt/models/residential_permits_to_parcels.sql
create mode 100644 dbt/models/usps_migration.sql
create mode 100644 dbt/models/zip_codes.sql
create mode 100644 dbt/package-lock.yml
create mode 100644 dbt/packages.yml
create mode 100644 dbt/seeds/.gitkeep
create mode 100644 dbt/seeds/region_tag_type.csv
create mode 100644 dbt/snapshots/.gitkeep
create mode 100644 dbt/tests/.gitkeep
diff --git a/dbt/.gitignore b/dbt/.gitignore
new file mode 100644
index 00000000..23e952a5
--- /dev/null
+++ b/dbt/.gitignore
@@ -0,0 +1,3 @@
+target/
+dbt_packages/
+logs/
\ No newline at end of file
diff --git a/dbt/README.md b/dbt/README.md
new file mode 100644
index 00000000..7874ac84
--- /dev/null
+++ b/dbt/README.md
@@ -0,0 +1,15 @@
+Welcome to your new dbt project!
+
+### Using the starter project
+
+Try running the following commands:
+- dbt run
+- dbt test
+
+
+### Resources:
+- Learn more about dbt [in the docs](https://docs.getdbt.com/docs/introduction)
+- Check out [Discourse](https://discourse.getdbt.com/) for commonly asked questions and answers
+- Join the [chat](https://community.getdbt.com/) on Slack for live discussions and support
+- Find [dbt events](https://events.getdbt.com) near you
+- Check out [the blog](https://blog.getdbt.com/) for the latest news on dbt's development and best practices
diff --git a/dbt/analyses/.gitkeep b/dbt/analyses/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/dbt/dbt_project.yml b/dbt/dbt_project.yml
new file mode 100644
index 00000000..e4b65a64
--- /dev/null
+++ b/dbt/dbt_project.yml
@@ -0,0 +1,28 @@
+
+# Name your project! Project names should contain only lowercase characters
+# and underscores. A good package name should reflect your organization's
+# name or the intended use of these models
+name: 'cities'
+version: '1.0.0'
+
+# This setting configures which "profile" dbt uses for this project.
+profile: 'cities'
+
+# These configurations specify where dbt should look for different types of files.
+# The `model-paths` config, for example, states that models in this project can be
+# found in the "models/" directory. You probably won't need to change these!
+model-paths: ["models"]
+analysis-paths: ["analyses"]
+test-paths: ["tests"]
+seed-paths: ["seeds"]
+macro-paths: ["macros"]
+snapshot-paths: ["snapshots"]
+
+clean-targets: # directories to be removed by `dbt clean`
+ - "target"
+ - "dbt_packages"
+
+
+vars:
+ # years for which we have census tract/block group data
+ census_years: [2010, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]
diff --git a/dbt/macros/.gitkeep b/dbt/macros/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/dbt/macros/tag_regions.sql b/dbt/macros/tag_regions.sql
new file mode 100644
index 00000000..a9c64bae
--- /dev/null
+++ b/dbt/macros/tag_regions.sql
@@ -0,0 +1,67 @@
+-- Tag regions with their containing/most intersecting/closest parent regions.
+-- child_table: table with the child regions
+-- parent_table: table with the parent regions
+-- max_distance: maximum distance to consider a region as a parent (meters)
+{% macro tag_regions(child_table, parent_table, max_distance=100) %}
+(
+with child as (
+ select * from {{child_table}}
+)
+, parent as (
+ select * from {{parent_table}}
+)
+, within as (
+ select child.id as child_id
+ , parent.id as parent_id
+ , child.valid * parent.valid as valid
+ from
+ child
+ inner join parent
+ on ST_Within (child.geom, parent.geom)
+ and child.valid && parent.valid
+)
+, not_within as (
+ select * from child
+ where not exists (select child_id from within where child_id = id)
+)
+, largest_overlap as (
+ select distinct on (child.id)
+ child.id as child_id
+ , parent.id as parent_id
+ , child.valid * parent.valid as valid
+ from
+ not_within as child
+ inner join parent
+ on ST_Intersects (child.geom, parent.geom)
+ and child.valid && parent.valid
+ order by
+ child_id,
+ ST_Area (ST_Intersection (child.geom, parent.geom)) desc
+)
+, no_overlap as (
+ select * from not_within
+ where not exists (
+ select child_id from largest_overlap where child_id = id
+ )
+)
+, closest as (
+ select distinct on (child.id)
+ child.id as child_id
+ , parent.id as parent_id
+ , child.valid * parent.valid as valid
+ from
+ no_overlap as child
+ inner join parent
+ on child.valid && parent.valid
+ and ST_DWithin (child.geom, parent.geom, {{max_distance}})
+ order by
+ child_id,
+ ST_Distance (child.geom, parent.geom)
+)
+select *, 'within' as type_ from within
+union all
+select *, 'most_overlap' as type_ from largest_overlap
+union all
+select *, 'closest' as type_ from closest
+)
+{% endmacro %}
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
new file mode 100644
index 00000000..382d69d5
--- /dev/null
+++ b/dbt/models/acs_block_group.sql
@@ -0,0 +1,23 @@
+with
+census_block_groups as (
+ select
+ census_block_group_id
+ , statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , valid
+ from
+ {{ ref('census_block_groups') }}
+)
+
+select
+ census_block_group_id
+ , year_
+ , name_
+ , value_
+from
+ acs_bg_raw
+ inner join census_block_groups using (statefp, countyfp, tractce, blkgrpce)
+where
+ to_date(acs_bg_raw.year_::text , 'YYYY') <@ census_block_groups.valid
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
new file mode 100644
index 00000000..f71b7088
--- /dev/null
+++ b/dbt/models/acs_tract.sql
@@ -0,0 +1,23 @@
+with
+census_tracts as (
+ select
+ census_tract_id
+ , statefp
+ , countyfp
+ , tractce
+ , valid
+
+ from {{ ref("census_tracts") }}
+)
+
+select
+ census_tract_id
+ , acs_tract_raw.year_
+ , acs_tract_raw.name_
+ , acs_tract_raw.value_
+from
+ acs_tract_raw
+ inner join census_tracts
+ using (statefp, countyfp, tractce)
+ where
+ to_date(acs_tract_raw.year_::text , 'YYYY') <@ census_tracts.valid
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
new file mode 100644
index 00000000..766a2c7c
--- /dev/null
+++ b/dbt/models/census_block_groups.sql
@@ -0,0 +1,53 @@
+with
+census_tracts as (
+ select
+ census_tract_id
+ , statefp
+ , countyfp
+ , tractce
+ , valid
+ from {{ ref("census_tracts") }}
+),
+census_block_groups as (
+ {% for year_ in var('census_years') %}
+ select
+ {{ 'statefp' if year_ >= 2013 else 'state' }} as statefp
+ , {{ 'countyfp' if year_ >= 2013 else 'county' }} as countyfp
+ , {{ 'tractce' if year_ >= 2013 else 'tract' }} as tractce
+ , {{ 'blkgrpce' if year_ >= 2013 else 'blkgrp' }} as blkgrpce
+ , {{ 'geoidfq' if year_ >= 2023 else
+ 'affgeoid' if year_ >= 2013 else
+ 'geo_id' }} as geoidfq
+ , '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
+ , geom
+ from
+ minneapolis.cb_{{ year_ }}_27_bg_500k
+ {% if not loop.last %}union all{% endif %}
+ {% endfor %}
+),
+census_block_groups_with_tracts as (
+ select
+ census_block_groups.statefp
+ , census_block_groups.countyfp
+ , census_block_groups.tractce
+ , census_block_groups.blkgrpce
+ , census_block_groups.geoidfq
+ , census_tracts.census_tract_id
+ , (census_block_groups.valid * census_tracts.valid) as valid
+ , census_block_groups.geom
+ from census_block_groups
+ inner join census_tracts using (statefp , countyfp , tractce)
+ where
+ census_tracts.valid && census_block_groups.valid
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_block_group_id
+ , statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , geoidfq
+ , census_tract_id
+ , valid
+ , geom
+from census_block_groups_with_tracts
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
new file mode 100644
index 00000000..c6c40cff
--- /dev/null
+++ b/dbt/models/census_tracts.sql
@@ -0,0 +1,26 @@
+with census_tracts as (
+{% for year_ in var('census_years') %}
+select
+ {{ 'statefp' if year_ >= 2013 else 'state' }} as statefp
+ , {{ 'countyfp' if year_ >= 2013 else 'county' }} as countyfp
+ , {{ 'tractce' if year_ >= 2013 else 'tract' }} as tractce
+ , {{ 'geoidfq' if year_ >= 2023 else
+ 'affgeoid' if year_ >= 2013 else
+ 'geo_id' }} as geoidfq
+ , '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
+ , geom
+from
+ minneapolis.cb_{{ year_ }}_27_tract_500k
+{% if not loop.last %}union all{% endif %}
+{% endfor %}
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_tract_id
+ , statefp
+ , countyfp
+ , tractce
+ , geoidfq
+ , valid
+ , geom
+from
+ census_tracts
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
new file mode 100644
index 00000000..d3eb1f74
--- /dev/null
+++ b/dbt/models/commercial_permits.sql
@@ -0,0 +1,13 @@
+with
+commercial_permits_to_parcels as (
+ select
+ commercial_permit_id
+ , parcel_id
+ from {{ ref("commercial_permits_to_parcels") }}
+)
+select
+ {{ dbt_utils.star(ref('commercial_permits_base')) }}
+ , parcel_id
+from
+ {{ ref('commercial_permits_base') }}
+ left join commercial_permits_to_parcels using (commercial_permit_id)
diff --git a/dbt/models/commercial_permits_base.sql b/dbt/models/commercial_permits_base.sql
new file mode 100644
index 00000000..100bdc32
--- /dev/null
+++ b/dbt/models/commercial_permits_base.sql
@@ -0,0 +1,18 @@
+select
+ sde_id as commercial_permit_id
+ , year::int as year_
+ , nonres_gro as group_
+ , nonres_sub as subgroup
+ , nonres_typ as type_category
+ , bldg_name as building_name
+ , bldg_desc as building_description
+ , permit_typ as permit_type
+ , permit_val as permit_value
+ , sqf as square_feet
+ , address
+ , geom
+ from
+ commercial_permits_raw
+ where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/commercial_permits_to_parcels.sql b/dbt/models/commercial_permits_to_parcels.sql
new file mode 100644
index 00000000..7c31e7ca
--- /dev/null
+++ b/dbt/models/commercial_permits_to_parcels.sql
@@ -0,0 +1,21 @@
+with
+commercial_permits as (
+ select
+ commercial_permit_id as id
+ , daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
+ , geom
+ from {{ ref("commercial_permits_base") }}
+)
+, parcels as (
+ select
+ parcel_id as id
+ , valid
+ , geom
+ from {{ ref("parcels") }}
+)
+select
+ child_id as commercial_permit_id
+ , parent_id as parcel_id
+ , valid
+ , type_
+from {{ tag_regions("commercial_permits", "parcels") }}
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
new file mode 100644
index 00000000..605f040c
--- /dev/null
+++ b/dbt/models/fair_market_rents.sql
@@ -0,0 +1,32 @@
+{% set num_bedrooms = range(0, 5) %}
+
+with
+zip_codes as (
+ select
+ zip_code_id
+ , zip_code
+ , valid
+ from {{ ref('zip_codes') }}
+)
+, fmr_zip as (
+ select
+ zip_codes.zip_code_id
+ {% for bedroom in num_bedrooms %}
+ , fair_market_rents_raw.rent_br{{ bedroom }}
+ {% endfor %}
+ , fair_market_rents_raw.year_
+ from
+ fair_market_rents_raw
+ inner join zip_codes
+ on zip_codes.zip_code = fair_market_rents_raw.zip
+ and zip_codes.valid @> to_date(year_::text , 'YYYY')
+)
+{% for bedroom in num_bedrooms %}
+select
+ zip_code_id
+ , rent_br{{ bedroom }} as rent
+ , 0 as num_bedrooms
+ , year_
+from fmr_zip
+{% if not loop.last %} union all {% endif %}
+{% endfor %}
diff --git a/dbt/models/parcels.sql b/dbt/models/parcels.sql
new file mode 100644
index 00000000..f3482927
--- /dev/null
+++ b/dbt/models/parcels.sql
@@ -0,0 +1,21 @@
+with
+parcels_to_zip_codes as (
+ select
+ parcel_id
+ , zip_code_id
+ from {{ref('parcels_to_zip_codes')}}
+),
+parcels_to_census_block_groups as (
+ select
+ parcel_id
+ , census_block_group_id
+ from {{ref('parcels_to_census_block_groups')}}
+)
+select
+ {{ dbt_utils.star(ref('parcels_base')) }}
+ , zip_code_id
+ , census_block_group_id
+from
+ {{ ref('parcels_base') }}
+ left join parcels_to_zip_codes using (parcel_id)
+ left join parcels_to_census_block_groups using (parcel_id)
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
new file mode 100644
index 00000000..f1cdbf36
--- /dev/null
+++ b/dbt/models/parcels_base.sql
@@ -0,0 +1,33 @@
+{% set years = range(2002, 2024) %}
+{% set city = 'MINNEAPOLIS' %}
+{% set county_id = '053' %}
+
+with parcels as (
+ {% for year_ in years %}
+ select
+ replace(pin, '{{ county_id }}-', '') as pin,
+ '[{{ year_ - 1 }}-01-01,{{ year_ }}-01-01)'::daterange as valid,
+ nullif(emv_land, 0) as emv_land,
+ nullif(emv_bldg, 0) as emv_bldg,
+ nullif(emv_total, 0) as emv_total,
+ nullif(year_built, 0) as year_built,
+ sale_date,
+ nullif(sale_value, 0) as sale_value,
+ geom
+ from minneapolis.parcels{{ year_ }}hennepin
+ where upper({{ "city" if year_ < 2018 else "ctu_name" }}) = '{{ city }}'
+ {% if not loop.last %}union all{% endif %}
+ {% endfor %}
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['pin', 'valid']) }} as parcel_id
+ , pin
+ , valid
+ , emv_land
+ , emv_bldg
+ , emv_total
+ , year_built
+ , sale_date
+ , sale_value
+ , geom
+from parcels
diff --git a/dbt/models/parcels_to_census_block_groups.sql b/dbt/models/parcels_to_census_block_groups.sql
new file mode 100644
index 00000000..2f2bd0f8
--- /dev/null
+++ b/dbt/models/parcels_to_census_block_groups.sql
@@ -0,0 +1,21 @@
+with
+parcels as (
+ select
+ parcel_id as id
+ , valid
+ , ST_Transform(geom, 4269) as geom
+ from {{ ref("parcels_base") }}
+),
+census_block_groups as (
+ select
+ census_block_group_id as id
+ , valid
+ , geom
+ from {{ ref("census_block_groups") }}
+)
+select
+ child_id as parcel_id
+ , parent_id as census_block_group_id
+ , valid
+ , type_
+from {{ tag_regions("parcels", "census_block_groups") }}
diff --git a/dbt/models/parcels_to_zip_codes.sql b/dbt/models/parcels_to_zip_codes.sql
new file mode 100644
index 00000000..aac320c0
--- /dev/null
+++ b/dbt/models/parcels_to_zip_codes.sql
@@ -0,0 +1,21 @@
+with
+parcels as (
+ select
+ parcel_id as id
+ , valid
+ , ST_Transform(geom, 4269) as geom
+ from {{ ref("parcels_base") }}
+),
+zip_codes as (
+ select
+ zip_code_id as id
+ , valid
+ , geom
+ from {{ ref("zip_codes") }}
+)
+select
+ child_id as parcel_id
+ , parent_id as zip_code_id
+ , valid
+ , type_
+from {{ tag_regions("parcels", "zip_codes") }}
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
new file mode 100644
index 00000000..869e41d9
--- /dev/null
+++ b/dbt/models/residential_permits.sql
@@ -0,0 +1,13 @@
+with
+residential_permits_to_parcels as (
+ select
+ residential_permit_id
+ , parcel_id
+ from {{ ref("residential_permits_to_parcels") }}
+)
+select
+ {{ dbt_utils.star(ref('residential_permits_base')) }}
+ , parcel_id
+from
+ {{ ref('residential_permits_base') }}
+ left join residential_permits_to_parcels using (residential_permit_id)
diff --git a/dbt/models/residential_permits_base.sql b/dbt/models/residential_permits_base.sql
new file mode 100644
index 00000000..a5bf8e0b
--- /dev/null
+++ b/dbt/models/residential_permits_base.sql
@@ -0,0 +1,25 @@
+select
+ sde_id as residential_permit_id
+ , year::int as year_
+ , tenure
+ , housing_ty as housing_type
+ , res_permit as permit_type
+ , address
+ , name as name_
+ , buildings as num_buildings
+ , units as num_units
+ , age_restri as num_age_restricted_units
+ , memory_car as num_memory_care_units
+ , assisted as num_assisted_living_units
+ , com_off_re = 'Y' as is_commercial_and_residential
+ , sqf as square_feet
+ , public_fun = 'Y' as is_public_funded
+ , permit_val as permit_value
+ , community_ as community_designation
+ , notes
+ , geom
+from
+ residential_permits_raw
+where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/residential_permits_to_parcels.sql b/dbt/models/residential_permits_to_parcels.sql
new file mode 100644
index 00000000..2c90dc32
--- /dev/null
+++ b/dbt/models/residential_permits_to_parcels.sql
@@ -0,0 +1,21 @@
+with
+residential_permits as (
+ select
+ residential_permit_id as id
+ , daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
+ , geom
+ from {{ ref("residential_permits_base") }}
+)
+, parcels as (
+ select
+ parcel_id as id
+ , valid
+ , geom
+ from {{ ref("parcels") }}
+)
+select
+ child_id as residential_permit_id
+ , parent_id as parcel_id
+ , valid
+ , type_
+from {{ tag_regions("residential_permits", "parcels") }}
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
new file mode 100644
index 00000000..031446ab
--- /dev/null
+++ b/dbt/models/usps_migration.sql
@@ -0,0 +1,44 @@
+{% set usps_migration_flow_types = ['business', 'family', 'individual', 'perm', 'temp'] %}
+{% set usps_migration_flow_directions = ['from', 'to'] %}
+
+with process_date as (
+ select to_date(yyyymm, 'YYYYMM') as date_, *
+ from usps_migration_raw
+)
+, zip_codes as (
+ select
+ zip_code_id
+ , zip_code
+ , valid
+ from
+ {{ ref('zip_codes') }}
+)
+, add_zip_id as (
+ select zip_code_id, process_date.*
+ from
+ process_date
+ inner join zip_codes
+ on zip_codes.zip_code = replace(process_date.zip_code, '=', '')
+ and process_date.date_ <@ zip_codes.valid
+)
+{% for flow_direction in usps_migration_flow_directions %}
+ select
+ date_
+ , zip_code_id
+ , '{{ flow_direction }}' as flow_direction
+ , 'total' as flow_type
+ , total_{{ flow_direction }}_zip as flow_value
+ from add_zip_id
+ union all
+ {% for flow_type in usps_migration_flow_types %}
+ select
+ date_
+ , zip_code_id
+ , '{{ flow_direction }}' as flow_direction
+ , '{{ flow_type }}' as flow_type
+ , total_{{ flow_direction }}_zip_{{ flow_type }} as flow_value
+ from add_zip_id
+ {% if not loop.last %} union all {% endif %}
+ {% endfor %}
+{% if not loop.last %} union all {% endif %}
+{% endfor %}
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
new file mode 100644
index 00000000..048f82f9
--- /dev/null
+++ b/dbt/models/zip_codes.sql
@@ -0,0 +1,20 @@
+with
+zip_codes as (
+select
+ zcta5ce20 as zip_code,
+ '[2020-01-01,)'::daterange as valid,
+ geom
+from zip_raw_2020
+union all
+select
+ zcta as zip_code,
+ '[2000-01-01,2020-01-01)'::daterange as valid,
+ ST_Transform(geom, 4269) as geom
+from zip_raw_2000
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id
+ , zip_code
+ , valid
+ , geom
+from zip_codes
diff --git a/dbt/package-lock.yml b/dbt/package-lock.yml
new file mode 100644
index 00000000..feb1453d
--- /dev/null
+++ b/dbt/package-lock.yml
@@ -0,0 +1,4 @@
+packages:
+ - git: https://github.com/dbt-labs/dbt-utils.git
+ revision: 85ade29c3e69bed3a13812c716c19eea9a0551c4
+sha1_hash: c4c136ad4314bafcbe374c3b08b8711f1da046b7
diff --git a/dbt/packages.yml b/dbt/packages.yml
new file mode 100644
index 00000000..4f2aa773
--- /dev/null
+++ b/dbt/packages.yml
@@ -0,0 +1,3 @@
+packages:
+ - git: "https://github.com/dbt-labs/dbt-utils.git"
+ revision: 1.2.0
diff --git a/dbt/seeds/.gitkeep b/dbt/seeds/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/dbt/seeds/region_tag_type.csv b/dbt/seeds/region_tag_type.csv
new file mode 100644
index 00000000..a85bccfd
--- /dev/null
+++ b/dbt/seeds/region_tag_type.csv
@@ -0,0 +1,4 @@
+type_
+within
+most_overlap
+closest
diff --git a/dbt/snapshots/.gitkeep b/dbt/snapshots/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/dbt/tests/.gitkeep b/dbt/tests/.gitkeep
new file mode 100644
index 00000000..e69de29b
From 6b43cc1a90dfd10b4b29e762c8acc9e6ee28fc98 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 10:20:28 -0400
Subject: [PATCH 032/142] remove unused seed
---
dbt/seeds/region_tag_type.csv | 4 ----
1 file changed, 4 deletions(-)
delete mode 100644 dbt/seeds/region_tag_type.csv
diff --git a/dbt/seeds/region_tag_type.csv b/dbt/seeds/region_tag_type.csv
deleted file mode 100644
index a85bccfd..00000000
--- a/dbt/seeds/region_tag_type.csv
+++ /dev/null
@@ -1,4 +0,0 @@
-type_
-within
-most_overlap
-closest
From 0eaf473eab17974cef59f8c974ffd90ee08d5fb4 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 10:20:39 -0400
Subject: [PATCH 033/142] add segregation index model
---
dbt/macros/safe_divide.sql | 3 +
dbt/models/segregation_indexes.sql | 110 ++++++++++++++++++++++++++++
dbt/seeds/population_categories.csv | 9 +++
3 files changed, 122 insertions(+)
create mode 100644 dbt/macros/safe_divide.sql
create mode 100644 dbt/models/segregation_indexes.sql
create mode 100644 dbt/seeds/population_categories.csv
diff --git a/dbt/macros/safe_divide.sql b/dbt/macros/safe_divide.sql
new file mode 100644
index 00000000..7d1d5723
--- /dev/null
+++ b/dbt/macros/safe_divide.sql
@@ -0,0 +1,3 @@
+{% macro safe_divide(num, dem) %}
+ (case when {{ dem }} = 0 then 0 else {{ num }} / {{ dem }} end)
+{% endmacro %}
diff --git a/dbt/models/segregation_indexes.sql b/dbt/models/segregation_indexes.sql
new file mode 100644
index 00000000..2d2c3cac
--- /dev/null
+++ b/dbt/models/segregation_indexes.sql
@@ -0,0 +1,110 @@
+-- Segregation index for each tract for each year, computed for each reference
+-- distribution.
+--
+-- The segregation index is the KL-divergence between the distribution of
+-- population in a tract and a reference distribution. For example, a tract that
+-- has many more white people than the average for the city will have a high
+-- segregation index for the 'average_city' distribution.
+with
+ categories as (
+ select category from {{ ref("population_categories") }}
+ )
+ , acs_tract as (
+ select
+ census_tract_id
+ , year_
+ , name_
+ , value_
+ from {{ ref("acs_tract") }}
+ )
+ , pop_tyc as
+ ( -- Population by tract, year, and category
+ select acs_tract.census_tract_id, acs_tract.year_, categories.category, acs_tract.value_
+ from acs_tract
+ join acs_variable using (name_)
+ join categories on categories.category = acs_variable.description
+ ),
+ pop_ty as
+ ( -- Population by tract and year (note: using 'population' variable instead of aggregating categories)
+ select census_tract_id, year_, value_
+ from acs_tract join acs_variable using (name_)
+ where acs_variable.description = 'population'
+ ),
+ pop_yc as
+ ( -- Population by year and category
+ select year_, category, sum(value_) as value_
+ from pop_tyc
+ group by year_, category
+ ),
+ pop_y as
+ ( -- Population by year
+ select year_, sum(value_) as value_
+ from pop_ty
+ group by year_
+ ),
+ dist_yc as
+ ( -- Distribution of population by year and category
+ select
+ pop_yc.year_,
+ pop_yc.category,
+ {{ safe_divide('pop_yc.value_', 'pop_y.value_') }} as value_
+ from pop_yc
+ inner join pop_y using (year_)
+ ),
+ dist_tyc as
+ ( -- Distribution of population by tract, year, and category
+ select
+ pop_tyc.census_tract_id,
+ pop_tyc.year_,
+ pop_tyc.category,
+ {{ safe_divide('pop_tyc.value_', 'pop_ty.value_') }} as value_
+ from pop_tyc
+ inner join pop_ty using (year_, census_tract_id)
+ ),
+ uniform_dist as
+ ( -- Uniform distribution across categories
+ with n_cat as (select count(*) as n_cat from categories)
+ select category, 1.0 / n_cat as value_
+ from categories, n_cat
+ ),
+ average_dist as
+ ( -- Average of the annual citywide distributions
+ select category, avg(value_) as value_
+ from dist_yc
+ group by category
+ )
+select
+ census_tract_id,
+ year_,
+ dist as distribution,
+ sum(case when p = 0 or q = 0 then 0 else p * ln(p / q) end) as segregation_index
+from
+ (
+ select
+ dist_tyc.census_tract_id,
+ dist_tyc.year_,
+ dist_tyc.value_ as p,
+ uniform_dist.value_ as q,
+ 'uniform' as dist
+ from dist_tyc
+ inner join uniform_dist using (category)
+ union all
+ select
+ dist_tyc.census_tract_id,
+ dist_tyc.year_,
+ dist_tyc.value_ as p,
+ dist_yc.value_ as q,
+ 'annual_city' as dist
+ from dist_tyc
+ inner join dist_yc using (year_, category)
+ union all
+ select
+ dist_tyc.census_tract_id,
+ dist_tyc.year_,
+ dist_tyc.value_ as p,
+ average_dist.value_ as q,
+ 'average_city' as dist
+ from dist_tyc
+ inner join average_dist using (category)
+ )
+group by census_tract_id, year_, dist
diff --git a/dbt/seeds/population_categories.csv b/dbt/seeds/population_categories.csv
new file mode 100644
index 00000000..79e93b14
--- /dev/null
+++ b/dbt/seeds/population_categories.csv
@@ -0,0 +1,9 @@
+category
+population_white_non_hispanic
+population_black_non_hispanic
+population_hispanic_or_latino
+population_asian_non_hispanic
+population_native_hawaiian_or_pacific_islander_non_hispanic
+population_american_indian_or_alaska_native_non_hispanic
+population_multiple_races_non_hispanic
+population_other_non_hispanic
From bcd84152182d2fc84ed754dde6042a9b3022fe6d Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:00:56 -0400
Subject: [PATCH 034/142] use 2010 census tract/bg data for all years before
2013
---
dbt/models/census_block_groups.sql | 27 +++++++++++++++++----------
dbt/models/census_tracts.sql | 20 +++++++++++++-------
2 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index 766a2c7c..cad69186 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -11,17 +11,24 @@ census_tracts as (
census_block_groups as (
{% for year_ in var('census_years') %}
select
- {{ 'statefp' if year_ >= 2013 else 'state' }} as statefp
- , {{ 'countyfp' if year_ >= 2013 else 'county' }} as countyfp
- , {{ 'tractce' if year_ >= 2013 else 'tract' }} as tractce
- , {{ 'blkgrpce' if year_ >= 2013 else 'blkgrp' }} as blkgrpce
- , {{ 'geoidfq' if year_ >= 2023 else
- 'affgeoid' if year_ >= 2013 else
- 'geo_id' }} as geoidfq
- , '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
- , geom
+ {% if year_ == 2010 %}
+ state as statefp
+ , county countyfp
+ , tract as tractce
+ , blkgrp as blkgrpce
+ , geo_id as geoidfq
+ , '[,2013-01-01)'::daterange as valid -- use 2010 data for all years before 2013
+ {% else %}
+ statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
+ , '[{{ year_ }}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
+ {% endif %}
+ , geom
from
- minneapolis.cb_{{ year_ }}_27_bg_500k
+ minneapolis.cb_{{ year_ }}_27_bg_500k
{% if not loop.last %}union all{% endif %}
{% endfor %}
),
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index c6c40cff..5d48b46b 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -1,13 +1,19 @@
with census_tracts as (
-{% for year_ in var('census_years') %}
+ {% for year_ in var('census_years') %}
select
- {{ 'statefp' if year_ >= 2013 else 'state' }} as statefp
- , {{ 'countyfp' if year_ >= 2013 else 'county' }} as countyfp
- , {{ 'tractce' if year_ >= 2013 else 'tract' }} as tractce
- , {{ 'geoidfq' if year_ >= 2023 else
- 'affgeoid' if year_ >= 2013 else
- 'geo_id' }} as geoidfq
+ {% if year_ == 2010 %}
+ state as statefp
+ , county as countyfp
+ , tract as tractce
+ , geo_id as geoidfq
+ , '[,2013-01-01)'::daterange as valid
+ {% else %}
+ statefp
+ , countyfp
+ , tractce
+ , {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
, '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
+{% endif %}
, geom
from
minneapolis.cb_{{ year_ }}_27_tract_500k
From f7e78b7a5c2d6efd13b11eb8667e55660ed874eb Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:01:31 -0400
Subject: [PATCH 035/142] split zip codes into three models and aggregate
regions to avoid duplicates
---
dbt/models/zip_codes.sql | 8 ++++----
dbt/models/zip_codes_2000.sql | 6 ++++++
dbt/models/zip_codes_2020.sql | 4 ++++
3 files changed, 14 insertions(+), 4 deletions(-)
create mode 100644 dbt/models/zip_codes_2000.sql
create mode 100644 dbt/models/zip_codes_2020.sql
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 048f82f9..4e75d2a7 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -1,16 +1,16 @@
with
zip_codes as (
select
- zcta5ce20 as zip_code,
+ zip_code,
'[2020-01-01,)'::daterange as valid,
geom
-from zip_raw_2020
+from {{ ref('zip_codes_2020') }}
union all
select
- zcta as zip_code,
+ zip_code,
'[2000-01-01,2020-01-01)'::daterange as valid,
ST_Transform(geom, 4269) as geom
-from zip_raw_2000
+from {{ ref('zip_codes_2000') }}
)
select
{{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id
diff --git a/dbt/models/zip_codes_2000.sql b/dbt/models/zip_codes_2000.sql
new file mode 100644
index 00000000..fd9219ee
--- /dev/null
+++ b/dbt/models/zip_codes_2000.sql
@@ -0,0 +1,6 @@
+select
+ zcta as zip_code,
+ ST_Union(geom) as geom
+from
+ zip_raw_2000
+group by zcta
diff --git a/dbt/models/zip_codes_2020.sql b/dbt/models/zip_codes_2020.sql
new file mode 100644
index 00000000..2bbc29e7
--- /dev/null
+++ b/dbt/models/zip_codes_2020.sql
@@ -0,0 +1,4 @@
+select
+ zcta5ce20 as zip_code,
+ geom
+from zip_raw_2020
From 43675a22a79cbcb50ecef9e4599cf1c689eb6bc9 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:02:26 -0400
Subject: [PATCH 036/142] switch to package syntax
---
dbt/package-lock.yml | 6 +++---
dbt/packages.yml | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/dbt/package-lock.yml b/dbt/package-lock.yml
index feb1453d..5e486a0d 100644
--- a/dbt/package-lock.yml
+++ b/dbt/package-lock.yml
@@ -1,4 +1,4 @@
packages:
- - git: https://github.com/dbt-labs/dbt-utils.git
- revision: 85ade29c3e69bed3a13812c716c19eea9a0551c4
-sha1_hash: c4c136ad4314bafcbe374c3b08b8711f1da046b7
+ - package: dbt-labs/dbt_utils
+ version: 1.2.0
+sha1_hash: d4f259856543b0ef301e0b3b0bbc94ccb6b12a54
diff --git a/dbt/packages.yml b/dbt/packages.yml
index 4f2aa773..b9609fcb 100644
--- a/dbt/packages.yml
+++ b/dbt/packages.yml
@@ -1,3 +1,3 @@
packages:
- - git: "https://github.com/dbt-labs/dbt-utils.git"
- revision: 1.2.0
+ - package: dbt-labs/dbt_utils
+ version: 1.2.0
From 31512934911c3a7bd3598f6adc96d47b75ca3fd9 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:03:27 -0400
Subject: [PATCH 037/142] improve performance of region tagging by adding
indexes
---
dbt/macros/tag_regions.sql | 6 ++++--
dbt/models/census_block_groups.sql | 11 +++++++++++
dbt/models/parcels_base.sql | 13 ++++++++++++-
dbt/models/parcels_to_census_block_groups.sql | 10 ++++++++++
dbt/models/parcels_to_zip_codes.sql | 6 ++++++
dbt/models/zip_codes.sql | 10 ++++++++++
6 files changed, 53 insertions(+), 3 deletions(-)
diff --git a/dbt/macros/tag_regions.sql b/dbt/macros/tag_regions.sql
index a9c64bae..ae76c040 100644
--- a/dbt/macros/tag_regions.sql
+++ b/dbt/macros/tag_regions.sql
@@ -4,10 +4,12 @@
-- max_distance: maximum distance to consider a region as a parent (meters)
{% macro tag_regions(child_table, parent_table, max_distance=100) %}
(
-with child as (
+-- the not materialized keyword allows us to use indexes on the child and parent
+-- tables
+with child as not materialized (
select * from {{child_table}}
)
-, parent as (
+, parent as not materialized (
select * from {{parent_table}}
)
, within as (
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index cad69186..20f3a089 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -1,3 +1,14 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_block_group_id'], 'unique': true},
+ {'columns': ['geom'], 'type': 'gist'},
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
with
census_tracts as (
select
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index f1cdbf36..904cd6bf 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -1,3 +1,13 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parcel_id'], 'unique': true},
+ {'columns': ['geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
{% set years = range(2002, 2024) %}
{% set city = 'MINNEAPOLIS' %}
{% set county_id = '053' %}
@@ -5,6 +15,7 @@
with parcels as (
{% for year_ in years %}
select
+ ogc_fid,
replace(pin, '{{ county_id }}-', '') as pin,
'[{{ year_ - 1 }}-01-01,{{ year_ }}-01-01)'::daterange as valid,
nullif(emv_land, 0) as emv_land,
@@ -20,7 +31,7 @@ with parcels as (
{% endfor %}
)
select
- {{ dbt_utils.generate_surrogate_key(['pin', 'valid']) }} as parcel_id
+ {{ dbt_utils.generate_surrogate_key(['ogc_fid', 'valid']) }} as parcel_id
, pin
, valid
, emv_land
diff --git a/dbt/models/parcels_to_census_block_groups.sql b/dbt/models/parcels_to_census_block_groups.sql
index 2f2bd0f8..9215c72d 100644
--- a/dbt/models/parcels_to_census_block_groups.sql
+++ b/dbt/models/parcels_to_census_block_groups.sql
@@ -1,3 +1,13 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parcel_id']},
+ {'columns': ['census_block_group_id']}
+ ]
+ )
+}}
+
with
parcels as (
select
diff --git a/dbt/models/parcels_to_zip_codes.sql b/dbt/models/parcels_to_zip_codes.sql
index aac320c0..f97afcad 100644
--- a/dbt/models/parcels_to_zip_codes.sql
+++ b/dbt/models/parcels_to_zip_codes.sql
@@ -1,3 +1,9 @@
+{{
+ config(
+ materialized='table'
+ )
+}}
+
with
parcels as (
select
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 4e75d2a7..48180e1d 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -1,3 +1,13 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['zip_code_id'], 'unique': true},
+ {'columns': ['geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
with
zip_codes as (
select
From 50fc6f28f7befd5a2b3bb343117232f28ed7010d Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:04:05 -0400
Subject: [PATCH 038/142] add data tests
---
dbt/models/schema.yml | 181 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 181 insertions(+)
create mode 100644 dbt/models/schema.yml
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
new file mode 100644
index 00000000..eb57b33f
--- /dev/null
+++ b/dbt/models/schema.yml
@@ -0,0 +1,181 @@
+models:
+ - name: census_tracts
+ columns:
+ - name: census_tract_id
+ data_tests:
+ - unique
+ - not_null
+
+ - name: census_block_groups
+ columns:
+ - name: census_block_group_id
+ data_tests:
+ - unique
+ - not_null
+ - name: census_tract_id
+ data_tests:
+ - relationships:
+ to: ref('census_tracts')
+ field: census_tract_id
+
+ - name: acs_tract
+ data_tests:
+ - dbt_utils.unique_combination_of_columns:
+ combination_of_columns:
+ - census_tract_id
+ - year_
+ - name_
+ columns:
+ - name: census_tract_id
+ data_tests:
+ - relationships:
+ to: ref('census_tracts')
+ field: census_tract_id
+
+ - name: acs_block_group
+ data_tests:
+ - dbt_utils.unique_combination_of_columns:
+ combination_of_columns:
+ - census_block_group_id
+ - year_
+ - name_
+ columns:
+ - name: census_block_group_id
+ data_tests:
+ - relationships:
+ to: ref('census_block_groups')
+ field: census_block_group_id
+
+ - name: segregation_indexes
+ data_tests:
+ - dbt_utils.unique_combination_of_columns:
+ combination_of_columns:
+ - census_tract_id
+ - year_
+ - distribution
+ columns:
+ - name: census_tract_id
+ data_tests:
+ - relationships:
+ to: ref('census_tracts')
+ field: census_tract_id
+
+ - name: parcels
+ columns:
+ - name: parcel_id
+ data_tests:
+ - unique
+ - not_null
+ - name: zip_code_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('zip_codes')
+ field: zip_code_id
+ - name: census_block_group_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('census_block_groups')
+ field: census_block_group_id
+
+ - name: parcels_to_census_block_groups
+ data_tests:
+ - dbt_utils.unique_combination_of_columns:
+ combination_of_columns:
+ - parcel_id
+ - census_block_group_id
+ columns:
+ - name: parcel_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('parcels')
+ field: parcel_id
+ - name: census_block_group_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('census_block_groups')
+ field: census_block_group_id
+
+ - name: parcels_to_zip_codes
+ data_tests:
+ - dbt_utils.unique_combination_of_columns:
+ combination_of_columns:
+ - parcel_id
+ - zip_code_id
+ columns:
+ - name: parcel_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('parcels')
+ field: parcel_id
+ - name: zip_code_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('zip_codes')
+ field: zip_code_id
+
+ - name: zip_codes_2000
+ columns:
+ - name: zip_code
+ data_tests:
+ - not_null
+ - unique
+
+ - name: zip_codes_2020
+ columns:
+ - name: zip_code
+ data_tests:
+ - not_null
+ - unique
+
+ - name: zip_codes
+ columns:
+ - name: zip_code_id
+ data_tests:
+ - not_null
+ - unique
+
+ - name: usps_migration
+ data_tests:
+ - dbt_utils.unique_combination_of_columns:
+ combination_of_columns:
+ - parcel_id
+ - zip_code_id
+ columns:
+ - name: zip_code_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('zip_codes')
+ field: zip_code_id
+
+ - name: commercial_permits
+ columns:
+ - name: commercial_permit_id
+ data_tests:
+ - not_null
+ - unique
+ - name: parcel_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('parcels')
+ field: parcel_id
+
+ - name: residential_permits
+ columns:
+ - name: residential_permit_id
+ data_tests:
+ - not_null
+ - unique
+ - name: parcel_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('parcels')
+ field: parcel_id
From ef8024bf084c98df35ebdc0445ae4303ac542d5b Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:14:57 -0400
Subject: [PATCH 039/142] fix tests
---
dbt/models/schema.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index eb57b33f..6ef336f6 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -144,8 +144,10 @@ models:
data_tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- - parcel_id
+ - date_
- zip_code_id
+ - flow_direction
+ - flow_type
columns:
- name: zip_code_id
data_tests:
From 46de3ffba1fe68c002e14b83f26b564f388c5b3e Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:37:02 -0400
Subject: [PATCH 040/142] add sources when available
---
dbt/models/census_block_groups.sql | 2 +-
dbt/models/census_tracts.sql | 2 +-
dbt/models/city_boundary.sql | 4 ++
dbt/models/neighborhoods.sql | 6 +++
dbt/models/parcels_base.sql | 2 +-
dbt/models/schema.yml | 69 ++++++++++++++++++++++++++++++
dbt/models/wards.sql | 5 +++
7 files changed, 87 insertions(+), 3 deletions(-)
create mode 100644 dbt/models/city_boundary.sql
create mode 100644 dbt/models/neighborhoods.sql
create mode 100644 dbt/models/wards.sql
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index 20f3a089..6c0c31ce 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -39,7 +39,7 @@ census_block_groups as (
{% endif %}
, geom
from
- minneapolis.cb_{{ year_ }}_27_bg_500k
+ {{ source('minneapolis', 'cb_' ~ year_ ~ '_27_bg_500k') }}
{% if not loop.last %}union all{% endif %}
{% endfor %}
),
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 5d48b46b..05a79469 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -16,7 +16,7 @@ select
{% endif %}
, geom
from
- minneapolis.cb_{{ year_ }}_27_tract_500k
+ {{ source('minneapolis', 'cb_' ~ year_ ~ '_27_tract_500k') }}
{% if not loop.last %}union all{% endif %}
{% endfor %}
)
diff --git a/dbt/models/city_boundary.sql b/dbt/models/city_boundary.sql
new file mode 100644
index 00000000..fe44dbe0
--- /dev/null
+++ b/dbt/models/city_boundary.sql
@@ -0,0 +1,4 @@
+select
+ geom
+from
+ {{ source('minneapolis', 'minneapolis_city_boundary') }}
diff --git a/dbt/models/neighborhoods.sql b/dbt/models/neighborhoods.sql
new file mode 100644
index 00000000..9cc596bb
--- /dev/null
+++ b/dbt/models/neighborhoods.sql
@@ -0,0 +1,6 @@
+select
+ bdnum as neighborhood_id
+ , bdname as name_
+ , geom
+from
+ {{ source('minneapolis', 'minneapolis_neighborhoods') }}
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 904cd6bf..f8a6b1f8 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -25,7 +25,7 @@ with parcels as (
sale_date,
nullif(sale_value, 0) as sale_value,
geom
- from minneapolis.parcels{{ year_ }}hennepin
+ from {{ source('minneapolis', 'parcels' ~ year_ ~ 'hennepin') }}
where upper({{ "city" if year_ < 2018 else "ctu_name" }}) = '{{ city }}'
{% if not loop.last %}union all{% endif %}
{% endfor %}
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 6ef336f6..a6b20449 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -1,3 +1,58 @@
+sources:
+ - name: minneapolis
+ database: cities
+ schema: minneapolis
+ tables:
+ - name: parcels2002hennepin
+ - name: parcels2003hennepin
+ - name: parcels2004hennepin
+ - name: parcels2005hennepin
+ - name: parcels2006hennepin
+ - name: parcels2007hennepin
+ - name: parcels2008hennepin
+ - name: parcels2009hennepin
+ - name: parcels2010hennepin
+ - name: parcels2011hennepin
+ - name: parcels2012hennepin
+ - name: parcels2013hennepin
+ - name: parcels2014hennepin
+ - name: parcels2015hennepin
+ - name: parcels2016hennepin
+ - name: parcels2017hennepin
+ - name: parcels2018hennepin
+ - name: parcels2019hennepin
+ - name: parcels2020hennepin
+ - name: parcels2021hennepin
+ - name: parcels2022hennepin
+ - name: parcels2023hennepin
+ - name: cb_2010_27_bg_500k
+ - name: cb_2010_27_tract_500k
+ - name: cb_2013_27_bg_500k
+ - name: cb_2013_27_tract_500k
+ - name: cb_2014_27_bg_500k
+ - name: cb_2014_27_tract_500k
+ - name: cb_2015_27_bg_500k
+ - name: cb_2015_27_tract_500k
+ - name: cb_2016_27_bg_500k
+ - name: cb_2016_27_tract_500k
+ - name: cb_2017_27_bg_500k
+ - name: cb_2017_27_tract_500k
+ - name: cb_2018_27_bg_500k
+ - name: cb_2018_27_tract_500k
+ - name: cb_2019_27_bg_500k
+ - name: cb_2019_27_tract_500k
+ - name: cb_2020_27_bg_500k
+ - name: cb_2020_27_tract_500k
+ - name: cb_2021_27_bg_500k
+ - name: cb_2021_27_tract_500k
+ - name: cb_2022_27_bg_500k
+ - name: cb_2022_27_tract_500k
+ - name: cb_2023_27_bg_500k
+ - name: cb_2023_27_tract_500k
+ - name: minneapolis_city_boundary
+ - name: minneapolis_neighborhoods
+ - name: minneapolis_wards
+
models:
- name: census_tracts
columns:
@@ -181,3 +236,17 @@ models:
- relationships:
to: ref('parcels')
field: parcel_id
+
+ - name: neighborhoods
+ columns:
+ - name: neighborhood_id
+ data_tests:
+ - not_null
+ - unique
+
+ - name: wards
+ columns:
+ - name: ward_id
+ data_tests:
+ - not_null
+ - unique
diff --git a/dbt/models/wards.sql b/dbt/models/wards.sql
new file mode 100644
index 00000000..67f67211
--- /dev/null
+++ b/dbt/models/wards.sql
@@ -0,0 +1,5 @@
+select
+ bdnum as ward_id
+ , geom
+from
+ {{ source('minneapolis', 'minneapolis_wards') }}
From ed11162721847d05435866462a3d2008541b0de6 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 15:47:10 -0400
Subject: [PATCH 041/142] add remaining sources referencing old data loads
---
dbt/models/acs_block_group.sql | 15 ++++++-
dbt/models/acs_tract.sql | 2 +-
dbt/models/commercial_permits_base.sql | 2 +-
dbt/models/fair_market_rents.sql | 2 +-
dbt/models/residential_permits_base.sql | 2 +-
dbt/models/schema.yml | 56 +++++++++++++++----------
dbt/models/usps_migration.sql | 2 +-
dbt/models/zip_codes_2000.sql | 2 +-
dbt/models/zip_codes_2020.sql | 2 +-
9 files changed, 54 insertions(+), 31 deletions(-)
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index 382d69d5..24151720 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -10,14 +10,25 @@ census_block_groups as (
from
{{ ref('census_block_groups') }}
)
-
+, acs_bg as (
+ select
+ statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , year_
+ , name_
+ , value_
+ from
+ {{ source('minneapolis_old', 'acs_bg_raw') }}
+)
select
census_block_group_id
, year_
, name_
, value_
from
- acs_bg_raw
+ acs_bg
inner join census_block_groups using (statefp, countyfp, tractce, blkgrpce)
where
to_date(acs_bg_raw.year_::text , 'YYYY') <@ census_block_groups.valid
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index f71b7088..fc47e66d 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -16,7 +16,7 @@ select
, acs_tract_raw.name_
, acs_tract_raw.value_
from
- acs_tract_raw
+ {{ source('minneapolis_old', 'acs_tract_raw') }}
inner join census_tracts
using (statefp, countyfp, tractce)
where
diff --git a/dbt/models/commercial_permits_base.sql b/dbt/models/commercial_permits_base.sql
index 100bdc32..246a8c03 100644
--- a/dbt/models/commercial_permits_base.sql
+++ b/dbt/models/commercial_permits_base.sql
@@ -12,7 +12,7 @@ select
, address
, geom
from
- commercial_permits_raw
+ {{ source('minneapolis_old', 'commercial_permits_raw') }}
where
co_code = '053'
and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index 605f040c..e42fff62 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -16,7 +16,7 @@ zip_codes as (
{% endfor %}
, fair_market_rents_raw.year_
from
- fair_market_rents_raw
+ {{ source('minneapolis_old', 'fair_market_rents_raw') }}
inner join zip_codes
on zip_codes.zip_code = fair_market_rents_raw.zip
and zip_codes.valid @> to_date(year_::text , 'YYYY')
diff --git a/dbt/models/residential_permits_base.sql b/dbt/models/residential_permits_base.sql
index a5bf8e0b..455a8dde 100644
--- a/dbt/models/residential_permits_base.sql
+++ b/dbt/models/residential_permits_base.sql
@@ -19,7 +19,7 @@ select
, notes
, geom
from
- residential_permits_raw
+ {{ source('minneapolis_old', 'residential_permits_raw') }}
where
co_code = '053'
and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index a6b20449..02746939 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -1,30 +1,20 @@
sources:
+ - name: minneapolis_old
+ database: cities
+ schema: public
+ tables:
+ - name: acs_bg_raw
+ - name: acs_tract_raw
+ - name: commercial_permits_raw
+ - name: fair_market_rents_raw
+ - name: residential_permits_raw
+ - name: usps_migration
+ - name: zip_raw_2000
+ - name: zip_raw_2020
- name: minneapolis
database: cities
schema: minneapolis
tables:
- - name: parcels2002hennepin
- - name: parcels2003hennepin
- - name: parcels2004hennepin
- - name: parcels2005hennepin
- - name: parcels2006hennepin
- - name: parcels2007hennepin
- - name: parcels2008hennepin
- - name: parcels2009hennepin
- - name: parcels2010hennepin
- - name: parcels2011hennepin
- - name: parcels2012hennepin
- - name: parcels2013hennepin
- - name: parcels2014hennepin
- - name: parcels2015hennepin
- - name: parcels2016hennepin
- - name: parcels2017hennepin
- - name: parcels2018hennepin
- - name: parcels2019hennepin
- - name: parcels2020hennepin
- - name: parcels2021hennepin
- - name: parcels2022hennepin
- - name: parcels2023hennepin
- name: cb_2010_27_bg_500k
- name: cb_2010_27_tract_500k
- name: cb_2013_27_bg_500k
@@ -52,6 +42,28 @@ sources:
- name: minneapolis_city_boundary
- name: minneapolis_neighborhoods
- name: minneapolis_wards
+ - name: parcels2002hennepin
+ - name: parcels2003hennepin
+ - name: parcels2004hennepin
+ - name: parcels2005hennepin
+ - name: parcels2006hennepin
+ - name: parcels2007hennepin
+ - name: parcels2008hennepin
+ - name: parcels2009hennepin
+ - name: parcels2010hennepin
+ - name: parcels2011hennepin
+ - name: parcels2012hennepin
+ - name: parcels2013hennepin
+ - name: parcels2014hennepin
+ - name: parcels2015hennepin
+ - name: parcels2016hennepin
+ - name: parcels2017hennepin
+ - name: parcels2018hennepin
+ - name: parcels2019hennepin
+ - name: parcels2020hennepin
+ - name: parcels2021hennepin
+ - name: parcels2022hennepin
+ - name: parcels2023hennepin
models:
- name: census_tracts
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 031446ab..3a140eac 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -3,7 +3,7 @@
with process_date as (
select to_date(yyyymm, 'YYYYMM') as date_, *
- from usps_migration_raw
+ from {{ source('minneapolis_old', 'usps_migration') }}
)
, zip_codes as (
select
diff --git a/dbt/models/zip_codes_2000.sql b/dbt/models/zip_codes_2000.sql
index fd9219ee..d6b18b05 100644
--- a/dbt/models/zip_codes_2000.sql
+++ b/dbt/models/zip_codes_2000.sql
@@ -2,5 +2,5 @@ select
zcta as zip_code,
ST_Union(geom) as geom
from
- zip_raw_2000
+ {{ source('minneapolis_old', 'zip_raw_2000') }}
group by zcta
diff --git a/dbt/models/zip_codes_2020.sql b/dbt/models/zip_codes_2020.sql
index 2bbc29e7..038ac2c9 100644
--- a/dbt/models/zip_codes_2020.sql
+++ b/dbt/models/zip_codes_2020.sql
@@ -1,4 +1,4 @@
select
zcta5ce20 as zip_code,
geom
-from zip_raw_2020
+from {{ source('minneapolis_old', 'zip_raw_2020') }}
From 9d6863449a39aec29669f691c734ee96ffc6ad7d Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 16:35:14 -0400
Subject: [PATCH 042/142] create model for parking data
---
dbt/models/parking_base.sql | 30 ++++++++++++++++++++++++++++++
dbt/models/schema.yml | 1 +
2 files changed, 31 insertions(+)
create mode 100644 dbt/models/parking_base.sql
diff --git a/dbt/models/parking_base.sql b/dbt/models/parking_base.sql
new file mode 100644
index 00000000..6f4e6cdb
--- /dev/null
+++ b/dbt/models/parking_base.sql
@@ -0,0 +1,30 @@
+with
+ parking_raw as (
+ select
+ ogc_fid
+ , "date"
+ , "project na"
+ , address
+ , neighborho
+ , ward
+ , "downtown y"
+ , "housing un"
+ , "car parkin"
+ , "bike parki"
+ , "year"
+ , geom
+ from {{ source('minneapolis', 'parking_parcels') }}
+ )
+select
+ ogc_fid as parking_id
+ , to_date("year" || '-' || "date", 'YYYY-DD-Mon') as date_
+ , "project na" as project_name
+ , address
+ , neighborho as neighborhood
+ , ward
+ , "downtown y" = 'Y' as is_downtown
+ , "housing un" as num_housing_units
+ , "car parkin" as num_car_parking_spaces
+ , "bike parki" as num_bike_parking_spaces
+ , geom
+from parking_raw
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 02746939..7b11f865 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -64,6 +64,7 @@ sources:
- name: parcels2021hennepin
- name: parcels2022hennepin
- name: parcels2023hennepin
+ - name: parking_parcels
models:
- name: census_tracts
From b76e6b775970036194a2993921a76fd09ac0cebd Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 17:11:45 -0400
Subject: [PATCH 043/142] fix source
---
dbt/models/schema.yml | 2 +-
dbt/models/usps_migration.sql | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 7b11f865..fb829015 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -8,7 +8,7 @@ sources:
- name: commercial_permits_raw
- name: fair_market_rents_raw
- name: residential_permits_raw
- - name: usps_migration
+ - name: usps_migration_raw
- name: zip_raw_2000
- name: zip_raw_2020
- name: minneapolis
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 3a140eac..4fa46045 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -3,7 +3,7 @@
with process_date as (
select to_date(yyyymm, 'YYYYMM') as date_, *
- from {{ source('minneapolis_old', 'usps_migration') }}
+ from {{ source('minneapolis_old', 'usps_migration_raw') }}
)
, zip_codes as (
select
From c450b7d5c8e79e58ec2ccbfd95dceebfc0543aa9 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 17:12:12 -0400
Subject: [PATCH 044/142] add parking to parcel mapping
---
dbt/models/{parking_base.sql => parking.sql} | 0
dbt/models/parking_to_parcels.sql | 31 ++++++++++++++++++++
2 files changed, 31 insertions(+)
rename dbt/models/{parking_base.sql => parking.sql} (100%)
create mode 100644 dbt/models/parking_to_parcels.sql
diff --git a/dbt/models/parking_base.sql b/dbt/models/parking.sql
similarity index 100%
rename from dbt/models/parking_base.sql
rename to dbt/models/parking.sql
diff --git a/dbt/models/parking_to_parcels.sql b/dbt/models/parking_to_parcels.sql
new file mode 100644
index 00000000..994ef232
--- /dev/null
+++ b/dbt/models/parking_to_parcels.sql
@@ -0,0 +1,31 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parking_id']},
+ {'columns': ['parcel_id']}
+ ]
+ )
+}}
+
+with
+ parking as (
+ select
+ parking_id as id
+ , daterange(date_, date_, '[]') as valid
+ , ST_Transform(geom, 26915) as geom
+ from {{ ref('parking_base') }}
+ )
+ , parcels as (
+ select
+ parcel_id as id
+ , valid
+ , geom
+ from {{ ref('parcels_base') }}
+ )
+select
+ child_id as parking_id
+ , parent_id as parcel_id
+ , valid
+ , type_
+from {{ tag_regions("parking", "parcels") }}
From 516c56a59139715f2a76b99896770c61f28da1b2 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 17:12:30 -0400
Subject: [PATCH 045/142] add indexes to improve perf
---
dbt/models/parcels_to_census_block_groups.sql | 2 +-
dbt/models/parcels_to_zip_codes.sql | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/dbt/models/parcels_to_census_block_groups.sql b/dbt/models/parcels_to_census_block_groups.sql
index 9215c72d..07caa1fb 100644
--- a/dbt/models/parcels_to_census_block_groups.sql
+++ b/dbt/models/parcels_to_census_block_groups.sql
@@ -2,7 +2,7 @@
config(
materialized='table',
indexes = [
- {'columns': ['parcel_id']},
+ {'columns': ['parcel_id'], 'unique': true},
{'columns': ['census_block_group_id']}
]
)
diff --git a/dbt/models/parcels_to_zip_codes.sql b/dbt/models/parcels_to_zip_codes.sql
index f97afcad..2519888a 100644
--- a/dbt/models/parcels_to_zip_codes.sql
+++ b/dbt/models/parcels_to_zip_codes.sql
@@ -1,6 +1,10 @@
{{
config(
- materialized='table'
+ materialized='table',
+ indexes = [
+ {'columns': ['parcel_id'], 'unique': true},
+ {'columns': ['zip_code_id']}
+ ]
)
}}
From 310a6da78ff4ce9ddad4dde4915bba7d995577a5 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 17:12:40 -0400
Subject: [PATCH 046/142] fix source
---
dbt/models/acs_block_group.sql | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index 24151720..2e85556e 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -23,12 +23,12 @@ census_block_groups as (
{{ source('minneapolis_old', 'acs_bg_raw') }}
)
select
- census_block_group_id
- , year_
- , name_
- , value_
+ census_block_groups.census_block_group_id
+ , acs_bg.year_
+ , acs_bg.name_
+ , acs_bg.value_
from
acs_bg
inner join census_block_groups using (statefp, countyfp, tractce, blkgrpce)
where
- to_date(acs_bg_raw.year_::text , 'YYYY') <@ census_block_groups.valid
+ to_date(acs_bg.year_::text , 'YYYY') <@ census_block_groups.valid
From e0f9760a00a5b5ccef38026a4422ff597e98eb8e Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 17:33:13 -0400
Subject: [PATCH 047/142] more perf tuning
---
dbt/models/commercial_permits.sql | 39 +++++++++++-----
dbt/models/commercial_permits_base.sql | 18 --------
dbt/models/commercial_permits_to_parcels.sql | 14 +++++-
dbt/models/parking_to_parcels.sql | 2 +-
dbt/models/residential_permits.sql | 44 ++++++++++++++-----
dbt/models/residential_permits_base.sql | 25 -----------
dbt/models/residential_permits_to_parcels.sql | 14 +++++-
dbt/models/schema.yml | 24 ++++++++--
8 files changed, 106 insertions(+), 74 deletions(-)
delete mode 100644 dbt/models/commercial_permits_base.sql
delete mode 100644 dbt/models/residential_permits_base.sql
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index d3eb1f74..d3adfbe0 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -1,13 +1,28 @@
-with
-commercial_permits_to_parcels as (
- select
- commercial_permit_id
- , parcel_id
- from {{ ref("commercial_permits_to_parcels") }}
-)
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['commercial_permit_id'], 'unique': true},
+ {'columns': ['geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
select
- {{ dbt_utils.star(ref('commercial_permits_base')) }}
- , parcel_id
-from
- {{ ref('commercial_permits_base') }}
- left join commercial_permits_to_parcels using (commercial_permit_id)
+ sde_id as commercial_permit_id
+ , year::int as year_
+ , nonres_gro as group_
+ , nonres_sub as subgroup
+ , nonres_typ as type_category
+ , bldg_name as building_name
+ , bldg_desc as building_description
+ , permit_typ as permit_type
+ , permit_val as permit_value
+ , sqf as square_feet
+ , address
+ , geom
+ from
+ {{ source('minneapolis_old', 'commercial_permits_raw') }}
+ where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/commercial_permits_base.sql b/dbt/models/commercial_permits_base.sql
deleted file mode 100644
index 246a8c03..00000000
--- a/dbt/models/commercial_permits_base.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-select
- sde_id as commercial_permit_id
- , year::int as year_
- , nonres_gro as group_
- , nonres_sub as subgroup
- , nonres_typ as type_category
- , bldg_name as building_name
- , bldg_desc as building_description
- , permit_typ as permit_type
- , permit_val as permit_value
- , sqf as square_feet
- , address
- , geom
- from
- {{ source('minneapolis_old', 'commercial_permits_raw') }}
- where
- co_code = '053'
- and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/commercial_permits_to_parcels.sql b/dbt/models/commercial_permits_to_parcels.sql
index 7c31e7ca..de1df444 100644
--- a/dbt/models/commercial_permits_to_parcels.sql
+++ b/dbt/models/commercial_permits_to_parcels.sql
@@ -1,17 +1,27 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['commercial_permit_id']},
+ {'columns': ['parcel_id']}
+ ]
+ )
+}}
+
with
commercial_permits as (
select
commercial_permit_id as id
, daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
, geom
- from {{ ref("commercial_permits_base") }}
+ from {{ ref("commercial_permits") }}
)
, parcels as (
select
parcel_id as id
, valid
, geom
- from {{ ref("parcels") }}
+ from {{ ref("parcels_base") }}
)
select
child_id as commercial_permit_id
diff --git a/dbt/models/parking_to_parcels.sql b/dbt/models/parking_to_parcels.sql
index 994ef232..21c20edc 100644
--- a/dbt/models/parking_to_parcels.sql
+++ b/dbt/models/parking_to_parcels.sql
@@ -14,7 +14,7 @@ with
parking_id as id
, daterange(date_, date_, '[]') as valid
, ST_Transform(geom, 26915) as geom
- from {{ ref('parking_base') }}
+ from {{ ref('parking') }}
)
, parcels as (
select
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 869e41d9..3a4841bc 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -1,13 +1,35 @@
-with
-residential_permits_to_parcels as (
- select
- residential_permit_id
- , parcel_id
- from {{ ref("residential_permits_to_parcels") }}
-)
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['residential_permit_id'], 'unique': true},
+ {'columns': ['geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
select
- {{ dbt_utils.star(ref('residential_permits_base')) }}
- , parcel_id
+ sde_id as residential_permit_id
+ , year::int as year_
+ , tenure
+ , housing_ty as housing_type
+ , res_permit as permit_type
+ , address
+ , name as name_
+ , buildings as num_buildings
+ , units as num_units
+ , age_restri as num_age_restricted_units
+ , memory_car as num_memory_care_units
+ , assisted as num_assisted_living_units
+ , com_off_re = 'Y' as is_commercial_and_residential
+ , sqf as square_feet
+ , public_fun = 'Y' as is_public_funded
+ , permit_val as permit_value
+ , community_ as community_designation
+ , notes
+ , geom
from
- {{ ref('residential_permits_base') }}
- left join residential_permits_to_parcels using (residential_permit_id)
+ {{ source('minneapolis_old', 'residential_permits_raw') }}
+where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/residential_permits_base.sql b/dbt/models/residential_permits_base.sql
deleted file mode 100644
index 455a8dde..00000000
--- a/dbt/models/residential_permits_base.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-select
- sde_id as residential_permit_id
- , year::int as year_
- , tenure
- , housing_ty as housing_type
- , res_permit as permit_type
- , address
- , name as name_
- , buildings as num_buildings
- , units as num_units
- , age_restri as num_age_restricted_units
- , memory_car as num_memory_care_units
- , assisted as num_assisted_living_units
- , com_off_re = 'Y' as is_commercial_and_residential
- , sqf as square_feet
- , public_fun = 'Y' as is_public_funded
- , permit_val as permit_value
- , community_ as community_designation
- , notes
- , geom
-from
- {{ source('minneapolis_old', 'residential_permits_raw') }}
-where
- co_code = '053'
- and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/residential_permits_to_parcels.sql b/dbt/models/residential_permits_to_parcels.sql
index 2c90dc32..7f9ea59c 100644
--- a/dbt/models/residential_permits_to_parcels.sql
+++ b/dbt/models/residential_permits_to_parcels.sql
@@ -1,17 +1,27 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['residential_permit_id']},
+ {'columns': ['parcel_id']}
+ ]
+ )
+}}
+
with
residential_permits as (
select
residential_permit_id as id
, daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
, geom
- from {{ ref("residential_permits_base") }}
+ from {{ ref("residential_permits") }}
)
, parcels as (
select
parcel_id as id
, valid
, geom
- from {{ ref("parcels") }}
+ from {{ ref("parcels_base") }}
)
select
child_id as residential_permit_id
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index fb829015..7957c54b 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -230,6 +230,22 @@ models:
data_tests:
- not_null
- unique
+
+ - name: residential_permits
+ columns:
+ - name: residential_permit_id
+ data_tests:
+ - not_null
+ - unique
+
+ - name: residential_permits_to_parcels
+ columns:
+ - name: residential_permit_id
+ data_tests:
+ - not_null
+ - relationships:
+ to: ref('residential_permits')
+ field: residential_permit_id
- name: parcel_id
data_tests:
- not_null
@@ -237,12 +253,14 @@ models:
to: ref('parcels')
field: parcel_id
- - name: residential_permits
+ - name: commercial_permits_to_parcels
columns:
- - name: residential_permit_id
+ - name: commercial_permit_id
data_tests:
- not_null
- - unique
+ - relationships:
+ to: ref('commercial_permits')
+ field: commercial_permit_id
- name: parcel_id
data_tests:
- not_null
From 3ae366e008fec9ea30009d70846548f9da7e8f2e Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 14 Aug 2024 18:29:01 -0400
Subject: [PATCH 048/142] add postgrest config and example schema
---
api/postgrest.conf | 107 +++++++++++++++++++++++++++++++++++++++++++++
api/schema.sql | 33 ++++++++++++++
2 files changed, 140 insertions(+)
create mode 100644 api/postgrest.conf
create mode 100644 api/schema.sql
diff --git a/api/postgrest.conf b/api/postgrest.conf
new file mode 100644
index 00000000..097fba01
--- /dev/null
+++ b/api/postgrest.conf
@@ -0,0 +1,107 @@
+## Admin server used for checks. It's disabled by default unless a port is specified.
+# admin-server-port = 3001
+
+## The database role to use when no client authentication is provided
+db-anon-role = "web_anon"
+
+## Notification channel for reloading the schema cache
+db-channel = "pgrst"
+
+## Enable or disable the notification channel
+db-channel-enabled = true
+
+## Enable in-database configuration
+db-config = true
+
+## Function for in-database configuration
+## db-pre-config = "postgrest.pre_config"
+
+## Extra schemas to add to the search_path of every request
+db-extra-search-path = "public"
+
+## Limit rows in response
+# db-max-rows = 1000
+
+## Allow getting the EXPLAIN plan through the `Accept: application/vnd.pgrst.plan` header
+# db-plan-enabled = false
+
+## Number of open connections in the pool
+db-pool = 10
+
+## Time in seconds to wait to acquire a slot from the connection pool
+# db-pool-acquisition-timeout = 10
+
+## Time in seconds after which to recycle pool connections
+# db-pool-max-lifetime = 1800
+
+## Time in seconds after which to recycle unused pool connections
+# db-pool-max-idletime = 30
+
+## Allow automatic database connection retrying
+# db-pool-automatic-recovery = true
+
+## Stored proc to exec immediately after auth
+# db-pre-request = "stored_proc_name"
+
+## Enable or disable prepared statements. disabling is only necessary when behind a connection pooler.
+## When disabled, statements will be parametrized but won't be prepared.
+db-prepared-statements = true
+
+## The name of which database schema to expose to REST clients
+db-schemas = "api"
+
+## How to terminate database transactions
+## Possible values are:
+## commit (default)
+## Transaction is always committed, this can not be overriden
+## commit-allow-override
+## Transaction is committed, but can be overriden with Prefer tx=rollback header
+## rollback
+## Transaction is always rolled back, this can not be overriden
+## rollback-allow-override
+## Transaction is rolled back, but can be overriden with Prefer tx=commit header
+db-tx-end = "commit"
+
+## The standard connection URI format, documented at
+## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
+db-uri = "postgresql://postgres@34.123.100.76:5432/cities"
+
+# jwt-aud = "your_audience_claim"
+
+## Jspath to the role claim key
+jwt-role-claim-key = ".role"
+
+## Choose a secret, JSON Web Key (or set) to enable JWT auth
+## (use "@filename" to load from separate file)
+# jwt-secret = "secret_with_at_least_32_characters"
+jwt-secret-is-base64 = false
+
+## Enables and set JWT Cache max lifetime, disables caching with 0
+# jwt-cache-max-lifetime = 0
+
+## Logging level, the admitted values are: crit, error, warn, info and debug.
+log-level = "error"
+
+## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
+## Admitted values: follow-privileges, ignore-privileges, disabled
+openapi-mode = "follow-privileges"
+
+## Base url for the OpenAPI output
+openapi-server-proxy-uri = ""
+
+## Configurable CORS origins
+# server-cors-allowed-origins = ""
+
+server-host = "!4"
+server-port = 3000
+
+## Allow getting the request-response timing information through the `Server-Timing` header
+server-timing-enabled = false
+
+## Unix socket location
+## if specified it takes precedence over server-port
+# server-unix-socket = "/tmp/pgrst.sock"
+
+## Unix socket file mode
+## When none is provided, 660 is applied by default
+# server-unix-socket-mode = "660"
diff --git a/api/schema.sql b/api/schema.sql
new file mode 100644
index 00000000..7167ade7
--- /dev/null
+++ b/api/schema.sql
@@ -0,0 +1,33 @@
+drop schema if exists api cascade;
+
+create schema api;
+
+create view api.parcels as (
+ select * from dbt.parcels
+);
+
+create view api.census_tracts as (
+ select * from dbt.census_tracts
+);
+
+create view api.census_block_groups as (
+ select * from dbt.census_block_groups
+);
+
+create view api.zip_codes as (
+ select * from dbt.zip_codes
+);
+
+create view api.emv_in_downtown_west as (
+ select dbt.parcels.pin, dbt.parcels.emv_land
+ from dbt.parcels
+ inner join dbt.neighborhoods
+ on st_intersects(st_transform(dbt.parcels.geom, 3857), dbt.neighborhoods.geom)
+ where dbt.neighborhoods.name_ = 'Downtown West'
+);
+
+drop role if exists web_anon;
+create role web_anon nologin;
+grant usage on schema api to web_anon;
+grant select on all tables in schema api to web_anon;
+grant web_anon to postgres;
From 2ae47247e7c8c83ad20582e482541709811d949a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 16 Aug 2024 12:50:45 -0400
Subject: [PATCH 049/142] import new sources and begin converting
---
dbt/models/census_block_groups.sql | 2 +-
dbt/models/census_tracts.sql | 2 +-
dbt/models/city_boundary.sql | 2 +-
dbt/models/commercial_permits.sql | 2 +-
dbt/models/fair_market_rents.sql | 21 +++--
dbt/models/neighborhoods.sql | 2 +-
dbt/models/parcels_base.sql | 2 +-
dbt/models/residential_permits.sql | 2 +-
dbt/models/schema.yml | 133 ++++++++++++++++++-----------
dbt/models/wards.sql | 2 +-
dbt/package-lock.yml | 4 +-
dbt/packages.yml | 2 +
12 files changed, 110 insertions(+), 66 deletions(-)
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index 6c0c31ce..a52c2f47 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -39,7 +39,7 @@ census_block_groups as (
{% endif %}
, geom
from
- {{ source('minneapolis', 'cb_' ~ year_ ~ '_27_bg_500k') }}
+ {{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_bg_500k') }}
{% if not loop.last %}union all{% endif %}
{% endfor %}
),
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 05a79469..634e18ac 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -16,7 +16,7 @@ select
{% endif %}
, geom
from
- {{ source('minneapolis', 'cb_' ~ year_ ~ '_27_tract_500k') }}
+ {{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_tract_500k') }}
{% if not loop.last %}union all{% endif %}
{% endfor %}
)
diff --git a/dbt/models/city_boundary.sql b/dbt/models/city_boundary.sql
index fe44dbe0..b34a22ec 100644
--- a/dbt/models/city_boundary.sql
+++ b/dbt/models/city_boundary.sql
@@ -1,4 +1,4 @@
select
geom
from
- {{ source('minneapolis', 'minneapolis_city_boundary') }}
+ {{ source('minneapolis', 'city_boundary_minneapolis') }}
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index d3adfbe0..349b6c8e 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -22,7 +22,7 @@ select
, address
, geom
from
- {{ source('minneapolis_old', 'commercial_permits_raw') }}
+ {{ source('minneapolis', 'commercial_permits_nonresidentialconstruction') }}
where
co_code = '053'
and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index e42fff62..9927b36f 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -8,24 +8,35 @@ zip_codes as (
, valid
from {{ ref('zip_codes') }}
)
+, fair_market_rents as (
+ select
+ zip_code
+ , rent_br0
+ , rent_br1
+ , rent_br2
+ , rent_br3
+ , rent_br4
+ , year_
+ from {{ ref('fair_market_rents_union') }}
+)
, fmr_zip as (
select
zip_codes.zip_code_id
{% for bedroom in num_bedrooms %}
- , fair_market_rents_raw.rent_br{{ bedroom }}
+ , fair_market_rents.rent_br{{ bedroom }}
{% endfor %}
- , fair_market_rents_raw.year_
+ , fair_market_rents.year_
from
- {{ source('minneapolis_old', 'fair_market_rents_raw') }}
+ fair_market_rents
inner join zip_codes
- on zip_codes.zip_code = fair_market_rents_raw.zip
+ on zip_codes.zip_code = fair_market_rents.zip_code
and zip_codes.valid @> to_date(year_::text , 'YYYY')
)
{% for bedroom in num_bedrooms %}
select
zip_code_id
, rent_br{{ bedroom }} as rent
- , 0 as num_bedrooms
+ , {{ bedroom }} as num_bedrooms
, year_
from fmr_zip
{% if not loop.last %} union all {% endif %}
diff --git a/dbt/models/neighborhoods.sql b/dbt/models/neighborhoods.sql
index 9cc596bb..b031cf08 100644
--- a/dbt/models/neighborhoods.sql
+++ b/dbt/models/neighborhoods.sql
@@ -3,4 +3,4 @@ select
, bdname as name_
, geom
from
- {{ source('minneapolis', 'minneapolis_neighborhoods') }}
+ {{ source('minneapolis', 'neighborhoods_minneapolis') }}
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index f8a6b1f8..3671a586 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -25,7 +25,7 @@ with parcels as (
sale_date,
nullif(sale_value, 0) as sale_value,
geom
- from {{ source('minneapolis', 'parcels' ~ year_ ~ 'hennepin') }}
+ from {{ source('minneapolis', 'parcels_shp_plan_regonal_' ~ year_ ~ '_parcels' ~ year_ ~ 'hennepin') }}
where upper({{ "city" if year_ < 2018 else "ctu_name" }}) = '{{ city }}'
{% if not loop.last %}union all{% endif %}
{% endfor %}
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 3a4841bc..95ee9a9d 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -29,7 +29,7 @@ select
, notes
, geom
from
- {{ source('minneapolis_old', 'residential_permits_raw') }}
+ {{ source('minneapolis', 'residential_permits_residentialpermits') }}
where
co_code = '053'
and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 7957c54b..3e1e334b 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -5,9 +5,6 @@ sources:
tables:
- name: acs_bg_raw
- name: acs_tract_raw
- - name: commercial_permits_raw
- - name: fair_market_rents_raw
- - name: residential_permits_raw
- name: usps_migration_raw
- name: zip_raw_2000
- name: zip_raw_2020
@@ -15,55 +12,87 @@ sources:
database: cities
schema: minneapolis
tables:
- - name: cb_2010_27_bg_500k
- - name: cb_2010_27_tract_500k
- - name: cb_2013_27_bg_500k
- - name: cb_2013_27_tract_500k
- - name: cb_2014_27_bg_500k
- - name: cb_2014_27_tract_500k
- - name: cb_2015_27_bg_500k
- - name: cb_2015_27_tract_500k
- - name: cb_2016_27_bg_500k
- - name: cb_2016_27_tract_500k
- - name: cb_2017_27_bg_500k
- - name: cb_2017_27_tract_500k
- - name: cb_2018_27_bg_500k
- - name: cb_2018_27_tract_500k
- - name: cb_2019_27_bg_500k
- - name: cb_2019_27_tract_500k
- - name: cb_2020_27_bg_500k
- - name: cb_2020_27_tract_500k
- - name: cb_2021_27_bg_500k
- - name: cb_2021_27_tract_500k
- - name: cb_2022_27_bg_500k
- - name: cb_2022_27_tract_500k
- - name: cb_2023_27_bg_500k
- - name: cb_2023_27_tract_500k
- - name: minneapolis_city_boundary
- - name: minneapolis_neighborhoods
- - name: minneapolis_wards
- - name: parcels2002hennepin
- - name: parcels2003hennepin
- - name: parcels2004hennepin
- - name: parcels2005hennepin
- - name: parcels2006hennepin
- - name: parcels2007hennepin
- - name: parcels2008hennepin
- - name: parcels2009hennepin
- - name: parcels2010hennepin
- - name: parcels2011hennepin
- - name: parcels2012hennepin
- - name: parcels2013hennepin
- - name: parcels2014hennepin
- - name: parcels2015hennepin
- - name: parcels2016hennepin
- - name: parcels2017hennepin
- - name: parcels2018hennepin
- - name: parcels2019hennepin
- - name: parcels2020hennepin
- - name: parcels2021hennepin
- - name: parcels2022hennepin
- - name: parcels2023hennepin
+ - name: residential_permits_residentialpermits
+ - name: commercial_permits_nonresidentialconstruction
+ - name: high_frequency_transit_2015_freq_350_ft_buffer
+ - name: high_frequency_transit_2015_freq_lines
+ - name: high_frequency_transit_2015_freq_quarter_and_half_mile_buffer
+ - name: high_frequency_transit_2015_freq_rail_stops
+ - name: high_frequency_transit_2016_freq_350_ft_buffer
+ - name: high_frequency_transit_2016_freq_lines
+ - name: high_frequency_transit_2016_freq_quarter_and_half_mile_buffer
+ - name: fair_market_rents_2012
+ - name: fair_market_rents_2013
+ - name: fair_market_rents_2014
+ - name: fair_market_rents_2015
+ - name: fair_market_rents_2016
+ - name: fair_market_rents_2017
+ - name: fair_market_rents_2018
+ - name: fair_market_rents_2019
+ - name: fair_market_rents_2020
+ - name: fair_market_rents_2021
+ - name: fair_market_rents_2022
+ - name: fair_market_rents_2023
+ - name: fair_market_rents_2024
+ - name: downtown
+ - name: university
+ - name: usps_y2018
+ - name: usps_y2019
+ - name: usps_y2020
+ - name: usps_y2021
+ - name: usps_y2022
+ - name: usps_y2023
+ - name: zip_codes_tl_2020_us_zcta510
+ - name: zip_codes_tl_2020_us_zcta520
+ - name: census_cb_2010_27_bg_500k
+ - name: census_cb_2010_27_tract_500k
+ - name: census_cb_2013_27_bg_500k
+ - name: census_cb_2013_27_tract_500k
+ - name: census_cb_2014_27_bg_500k
+ - name: census_cb_2014_27_tract_500k
+ - name: census_cb_2015_27_bg_500k
+ - name: census_cb_2015_27_tract_500k
+ - name: census_cb_2016_27_bg_500k
+ - name: census_cb_2016_27_tract_500k
+ - name: census_cb_2017_27_bg_500k
+ - name: census_cb_2017_27_tract_500k
+ - name: census_cb_2018_27_bg_500k
+ - name: census_cb_2018_27_tract_500k
+ - name: census_cb_2019_27_bg_500k
+ - name: census_cb_2019_27_tract_500k
+ - name: census_cb_2020_27_bg_500k
+ - name: census_cb_2020_27_tract_500k
+ - name: census_cb_2021_27_bg_500k
+ - name: census_cb_2021_27_tract_500k
+ - name: census_cb_2022_27_bg_500k
+ - name: census_cb_2022_27_tract_500k
+ - name: census_cb_2023_27_bg_500k
+ - name: census_cb_2023_27_tract_500k
+ - name: city_boundary_minneapolis
+ - name: neighborhoods_minneapolis
+ - name: wards_minneapolis
+ - name: parcels_shp_plan_regonal_2002_parcels2002hennepin
+ - name: parcels_shp_plan_regonal_2003_parcels2003hennepin
+ - name: parcels_shp_plan_regonal_2004_parcels2004hennepin
+ - name: parcels_shp_plan_regonal_2005_parcels2005hennepin
+ - name: parcels_shp_plan_regonal_2006_parcels2006hennepin
+ - name: parcels_shp_plan_regonal_2007_parcels2007hennepin
+ - name: parcels_shp_plan_regonal_2008_parcels2008hennepin
+ - name: parcels_shp_plan_regonal_2009_parcels2009hennepin
+ - name: parcels_shp_plan_regonal_2010_parcels2010hennepin
+ - name: parcels_shp_plan_regonal_2011_parcels2011hennepin
+ - name: parcels_shp_plan_regonal_2012_parcels2012hennepin
+ - name: parcels_shp_plan_regonal_2013_parcels2013hennepin
+ - name: parcels_shp_plan_regonal_2014_parcels2014hennepin
+ - name: parcels_shp_plan_regonal_2015_parcels2015hennepin
+ - name: parcels_shp_plan_regonal_2016_parcels2016hennepin
+ - name: parcels_shp_plan_regonal_2017_parcels2017hennepin
+ - name: parcels_shp_plan_regonal_2018_parcels2018hennepin
+ - name: parcels_shp_plan_regonal_2019_parcels2019hennepin
+ - name: parcels_shp_plan_regonal_2020_parcels2020hennepin
+ - name: parcels_shp_plan_regonal_2021_parcels2021hennepin
+ - name: parcels_shp_plan_regonal_2022_parcels2022hennepin
+ - name: parcels_shp_plan_regonal_2023_parcels2023hennepin
- name: parking_parcels
models:
diff --git a/dbt/models/wards.sql b/dbt/models/wards.sql
index 67f67211..d809d3ad 100644
--- a/dbt/models/wards.sql
+++ b/dbt/models/wards.sql
@@ -2,4 +2,4 @@ select
bdnum as ward_id
, geom
from
- {{ source('minneapolis', 'minneapolis_wards') }}
+ {{ source('minneapolis', 'wards_minneapolis') }}
diff --git a/dbt/package-lock.yml b/dbt/package-lock.yml
index 5e486a0d..5231cc02 100644
--- a/dbt/package-lock.yml
+++ b/dbt/package-lock.yml
@@ -1,4 +1,6 @@
packages:
- package: dbt-labs/dbt_utils
version: 1.2.0
-sha1_hash: d4f259856543b0ef301e0b3b0bbc94ccb6b12a54
+ - package: dbt-labs/codegen
+ version: 0.12.1
+sha1_hash: 37aba29ba147b9afff74716d974b60c54b7f1a1d
diff --git a/dbt/packages.yml b/dbt/packages.yml
index b9609fcb..27ef0473 100644
--- a/dbt/packages.yml
+++ b/dbt/packages.yml
@@ -1,3 +1,5 @@
packages:
- package: dbt-labs/dbt_utils
version: 1.2.0
+ - package: dbt-labs/codegen
+ version: 0.12.1
From a71845046f006cc7bd7126151560b648df933d21 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 16 Aug 2024 12:58:05 -0400
Subject: [PATCH 050/142] more conversion
---
dbt/models/fair_market_rents_union.sql | 15 +++++++++++++++
dbt/models/schema.yml | 1 -
dbt/models/usps_migration.sql | 4 ++--
dbt/models/usps_migration_union.sql | 23 +++++++++++++++++++++++
4 files changed, 40 insertions(+), 3 deletions(-)
create mode 100644 dbt/models/fair_market_rents_union.sql
create mode 100644 dbt/models/usps_migration_union.sql
diff --git a/dbt/models/fair_market_rents_union.sql b/dbt/models/fair_market_rents_union.sql
new file mode 100644
index 00000000..696d0a34
--- /dev/null
+++ b/dbt/models/fair_market_rents_union.sql
@@ -0,0 +1,15 @@
+{% set years = range(2012, 2025) %}
+
+{% for year_ in years %}
+select
+ zip_code
+ , rent_br0
+ , rent_br1
+ , rent_br2
+ , rent_br3
+ , rent_br4
+ , year as year_
+from
+ {{ source('minneapolis', 'fair_market_rents_' ~ year_) }}
+{% if not loop.last %} union all {% endif %}
+{% endfor %}
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 3e1e334b..a1f5753f 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -5,7 +5,6 @@ sources:
tables:
- name: acs_bg_raw
- name: acs_tract_raw
- - name: usps_migration_raw
- name: zip_raw_2000
- name: zip_raw_2020
- name: minneapolis
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 4fa46045..6a5954e3 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -2,8 +2,8 @@
{% set usps_migration_flow_directions = ['from', 'to'] %}
with process_date as (
- select to_date(yyyymm, 'YYYYMM') as date_, *
- from {{ source('minneapolis_old', 'usps_migration_raw') }}
+ select to_date(yyyy_mm, 'YYYYMM') as date_, *
+ from {{ ref('usps_migration_union') }}
)
, zip_codes as (
select
diff --git a/dbt/models/usps_migration_union.sql b/dbt/models/usps_migration_union.sql
new file mode 100644
index 00000000..e1e790e7
--- /dev/null
+++ b/dbt/models/usps_migration_union.sql
@@ -0,0 +1,23 @@
+{% set years = range(2018, 2024) %}
+
+{% for year_ in years %}
+ select
+ "YYYYMM" as yyyy_mm
+ , "ZIPCODE" as zip_code
+ , "CITY" as city
+ , "STATE" as state_
+ , "TOTAL_FROM_ZIP" as total_from_zip
+ , "TOTAL_BUSINESS" as total_from_zip_business
+ , "TOTAL_FAMILY" as total_from_zip_family
+ , "TOTAL_INDIVIDUAL" as total_from_zip_individual
+ , "TOTAL_PERM" as total_from_zip_perm
+ , "TOTAL_TEMP" as total_from_zip_temp
+ , "TOTAL_TO_ZIP" as total_to_zip
+ , "TOTAL_BUSINESS_dup" as total_to_zip_business
+ , "TOTAL_FAMILY_dup" as total_to_zip_family
+ , "TOTAL_INDIVIDUAL_dup" as total_to_zip_individual
+ , "TOTAL_PERM_dup" as total_to_zip_perm
+ , "TOTAL_TEMP_dup" as total_to_zip_temp
+ from {{ source('minneapolis', 'usps_y' ~ year_) }}
+{% if not loop.last %} union all {% endif %}
+{% endfor %}
From 15827e31e22f749c47af634b81c395b46635c120 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 16 Aug 2024 13:38:10 -0400
Subject: [PATCH 051/142] more conversion
---
dbt/models/acs_block_group.sql | 9 ++++
dbt/models/acs_tract.sql | 10 +++-
dbt/models/all_zip_codes.sql | 20 +++++++
dbt/models/all_zip_codes_2010.sql | 5 ++
dbt/models/all_zip_codes_2020.sql | 4 ++
dbt/models/parcels_base.sql | 2 +-
dbt/models/schema.yml | 13 +++--
dbt/models/usps_migration.sql | 9 ++++
dbt/models/zip_codes.sql | 35 ++++++------
dbt/models/zip_codes_2000.sql | 6 ---
dbt/models/zip_codes_2020.sql | 4 --
dbt/seeds/acs_variables.csv | 90 +++++++++++++++++++++++++++++++
12 files changed, 172 insertions(+), 35 deletions(-)
create mode 100644 dbt/models/all_zip_codes.sql
create mode 100644 dbt/models/all_zip_codes_2010.sql
create mode 100644 dbt/models/all_zip_codes_2020.sql
delete mode 100644 dbt/models/zip_codes_2000.sql
delete mode 100644 dbt/models/zip_codes_2020.sql
create mode 100644 dbt/seeds/acs_variables.csv
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index 2e85556e..98545ebb 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -1,3 +1,12 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_block_group_id', 'year_', 'name_'], 'unique': true},
+ ]
+ )
+}}
+
with
census_block_groups as (
select
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index fc47e66d..52c9517c 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -1,3 +1,12 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract_id', 'year_', 'name_'], 'unique': true},
+ ]
+ )
+}}
+
with
census_tracts as (
select
@@ -9,7 +18,6 @@ census_tracts as (
from {{ ref("census_tracts") }}
)
-
select
census_tract_id
, acs_tract_raw.year_
diff --git a/dbt/models/all_zip_codes.sql b/dbt/models/all_zip_codes.sql
new file mode 100644
index 00000000..ac438099
--- /dev/null
+++ b/dbt/models/all_zip_codes.sql
@@ -0,0 +1,20 @@
+with
+zip_codes as (
+select
+ zip_code,
+ '[2020-01-01,)'::daterange as valid,
+ geom
+from {{ ref('all_zip_codes_2020') }}
+union all
+select
+ zip_code,
+ '[,2020-01-01)'::daterange as valid,
+ geom
+from {{ ref('all_zip_codes_2010') }}
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id
+ , zip_code
+ , valid
+ , geom
+from zip_codes
diff --git a/dbt/models/all_zip_codes_2010.sql b/dbt/models/all_zip_codes_2010.sql
new file mode 100644
index 00000000..8cdafd23
--- /dev/null
+++ b/dbt/models/all_zip_codes_2010.sql
@@ -0,0 +1,5 @@
+select
+ zcta5ce10 as zip_code,
+ geom
+from
+ {{ source('minneapolis', 'zip_codes_tl_2020_us_zcta510') }}
diff --git a/dbt/models/all_zip_codes_2020.sql b/dbt/models/all_zip_codes_2020.sql
new file mode 100644
index 00000000..aee015ae
--- /dev/null
+++ b/dbt/models/all_zip_codes_2020.sql
@@ -0,0 +1,4 @@
+select
+ zcta5ce20 as zip_code,
+ geom
+from {{ source('minneapolis', 'zip_codes_tl_2020_us_zcta520') }}
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 3671a586..8fb2f2dd 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -3,7 +3,7 @@
materialized='table',
indexes = [
{'columns': ['parcel_id'], 'unique': true},
- {'columns': ['geom'], 'type': 'gist'}
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
]
)
}}
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index a1f5753f..f78e1251 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -5,8 +5,6 @@ sources:
tables:
- name: acs_bg_raw
- name: acs_tract_raw
- - name: zip_raw_2000
- - name: zip_raw_2020
- name: minneapolis
database: cities
schema: minneapolis
@@ -215,20 +213,27 @@ models:
to: ref('zip_codes')
field: zip_code_id
- - name: zip_codes_2000
+ - name: all_zip_codes_2010
columns:
- name: zip_code
data_tests:
- not_null
- unique
- - name: zip_codes_2020
+ - name: all_zip_codes_2020
columns:
- name: zip_code
data_tests:
- not_null
- unique
+ - name: all_zip_codes
+ columns:
+ - name: zip_code_id
+ data_tests:
+ - not_null
+ - unique
+
- name: zip_codes
columns:
- name: zip_code_id
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 6a5954e3..7550f0d0 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -1,3 +1,12 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['date_', 'zip_code_id', 'flow_direction', 'flow_type'], 'unique': true},
+ ]
+ )
+}}
+
{% set usps_migration_flow_types = ['business', 'family', 'individual', 'perm', 'temp'] %}
{% set usps_migration_flow_directions = ['from', 'to'] %}
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 48180e1d..17eac722 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -3,28 +3,25 @@
materialized='table',
indexes = [
{'columns': ['zip_code_id'], 'unique': true},
- {'columns': ['geom'], 'type': 'gist'}
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
]
)
}}
-with
-zip_codes as (
-select
- zip_code,
- '[2020-01-01,)'::daterange as valid,
- geom
-from {{ ref('zip_codes_2020') }}
-union all
-select
- zip_code,
- '[2000-01-01,2020-01-01)'::daterange as valid,
- ST_Transform(geom, 4269) as geom
-from {{ ref('zip_codes_2000') }}
+with city_boundary as (
+ select
+ st_transform(geom, 4269) as geom
+ from
+ {{ ref('city_boundary') }}
)
select
- {{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id
- , zip_code
- , valid
- , geom
-from zip_codes
+ all_zip_codes.zip_code_id
+ , all_zip_codes.zip_code
+ , all_zip_codes.valid
+ , all_zip_codes.geom
+from
+ {{ ref('all_zip_codes') }} as all_zip_codes,
+ city_boundary
+where
+ st_intersects(all_zip_codes.geom, city_boundary.geom)
+ and st_area(st_intersection(all_zip_codes.geom, city_boundary.geom)) / st_area(all_zip_codes.geom) > 0.2
diff --git a/dbt/models/zip_codes_2000.sql b/dbt/models/zip_codes_2000.sql
deleted file mode 100644
index d6b18b05..00000000
--- a/dbt/models/zip_codes_2000.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-select
- zcta as zip_code,
- ST_Union(geom) as geom
-from
- {{ source('minneapolis_old', 'zip_raw_2000') }}
-group by zcta
diff --git a/dbt/models/zip_codes_2020.sql b/dbt/models/zip_codes_2020.sql
deleted file mode 100644
index 038ac2c9..00000000
--- a/dbt/models/zip_codes_2020.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-select
- zcta5ce20 as zip_code,
- geom
-from {{ source('minneapolis_old', 'zip_raw_2020') }}
diff --git a/dbt/seeds/acs_variables.csv b/dbt/seeds/acs_variables.csv
new file mode 100644
index 00000000..8cdeba7c
--- /dev/null
+++ b/dbt/seeds/acs_variables.csv
@@ -0,0 +1,90 @@
+variable,description
+B03002_003E,population_white_non_hispanic
+B03002_004E,population_black_non_hispanic
+B03002_005E,population_asian_non_hispanic
+B03002_006E,population_native_hawaiian_or_pacific_islander_non_hispanic
+B03002_007E,population_american_indian_or_alaska_native_non_hispanic
+B03002_008E,population_other_non_hispanic
+B03002_009E,population_multiple_races_non_hispanic
+B03002_010E,population_multiple_races_and_other_non_hispanic
+B07204_001E,geographic_mobility_total_responses
+B07204_002E,geographic_mobility_same_house_1_year_ago
+B07204_004E,geographic_mobility_different_house_1_year_ago_same_city
+B07204_005E,geographic_mobility_different_house_1_year_ago_same_county
+B07204_006E,geographic_mobility_different_house_1_year_ago_same_state
+B07204_007E,geographic_mobility_different_house_1_year_ago_same_country
+B07204_016E,geographic_mobility_different_house_1_year_ago_abroad
+B01003_001E,population
+B02001_002E,white
+B02001_003E,black
+B02001_004E,american_indian_or_alaska_native
+B02001_005E,asian
+B02001_006E,native_hawaiian_or_pacific_islander
+B03001_003E,population_hispanic_or_latino
+B02001_007E,other_race
+B02001_008E,multiple_races
+B02001_009E,multiple_races_and_other_race
+B02001_010E,two_or_more_races_excluding_other
+B02015_002E,east_asian_chinese
+B02015_003E,east_asian_hmong
+B02015_004E,east_asian_japanese
+B02015_005E,east_asian_korean
+B02015_006E,east_asian_mongolian
+B02015_007E,east_asian_okinawan
+B02015_008E,east_asian_taiwanese
+B02015_009E,east_asian_other
+B02015_010E,southeast_asian_burmese
+B02015_011E,southeast_asian_cambodian
+B02015_012E,southeast_asian_filipino
+B02015_013E,southeast_asian_indonesian
+B02015_014E,southeast_asian_laotian
+B02015_015E,southeast_asian_malaysian
+B02015_016E,southeast_asian_mien
+B02015_017E,southeast_asian_singaporean
+B02015_018E,southeast_asian_thai
+B02015_019E,southeast_asian_viet
+B02015_020E,southeast_asian_other
+B02015_021E,south_asian_asian_indian
+B02015_022E,south_asian_bangladeshi
+B02015_023E,south_asian_bhutanese
+B02015_024E,south_asian_nepalese
+B02015_025E,south_asian_pakistani
+B02015_026E,south_asian_sikh
+B02015_027E,south_asian_sri_lankan
+B02015_028E,south_asian_other
+B02015_029E,central_asian_kazakh
+B02015_030E,central_asian_uzbek
+B02015_031E,central_asian_other
+B02015_032E,other_asian_specified
+B02015_033E,other_asian_not_specified
+B19013_001E,median_household_income
+B19013A_001E,median_household_income_white
+B19013H_001E,median_household_income_white_non_hispanic
+B19013I_001E,median_household_income_hispanic
+B19013B_001E,median_household_income_black
+B19013C_001E,median_household_income_american_indian_or_alaska_native
+B19013D_001E,median_household_income_asian
+B19013E_001E,median_household_income_native_hawaiian_or_pacific_islander
+B19013F_001E,median_household_income_other_race
+B19013G_001E,median_household_income_multiple_races
+B19019_002E,median_household_income_1_person_households
+B19019_003E,median_household_income_2_person_households
+B19019_004E,median_household_income_3_person_households
+B19019_005E,median_household_income_4_person_households
+B19019_006E,median_household_income_5_person_households
+B19019_007E,median_household_income_6_person_households
+B19019_008E,median_household_income_7_or_more_person_households
+B01002_001E,median_age
+B01002_002E,median_age_male
+B01002_003E,median_age_female
+B25031_001E,median_gross_rent
+B25031_002E,median_gross_rent_0_bedrooms
+B25031_003E,median_gross_rent_1_bedrooms
+B25031_004E,median_gross_rent_2_bedrooms
+B25031_005E,median_gross_rent_3_bedrooms
+B25031_006E,median_gross_rent_4_bedrooms
+B25031_007E,median_gross_rent_5_bedrooms
+B25032_001E,total_housing_units
+B25032_002E,total_owner_occupied_housing_units
+B25032_013E,total_renter_occupied_housing_units
+B25070_001E,median_gross_rent_as_percentage_of_household_income
From 367ceb2e5d2c2cb29f541b3e4937ef817b01811c Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 16 Aug 2024 15:12:19 -0400
Subject: [PATCH 052/142] add acs tract wide table
---
dbt/models/acs_tract.sql | 20 +++++++---
dbt/models/acs_tract_clean.sql | 20 ++++++++++
dbt/models/acs_tract_wide.sql | 73 ++++++++++++++++++++++++++++++++++
3 files changed, 108 insertions(+), 5 deletions(-)
create mode 100644 dbt/models/acs_tract_clean.sql
create mode 100644 dbt/models/acs_tract_wide.sql
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index 52c9517c..7482f43a 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -18,14 +18,24 @@ census_tracts as (
from {{ ref("census_tracts") }}
)
+, acs_tract as (
+ select
+ statefp
+ , countyfp
+ , tractce
+ , year_
+ , name_
+ , value_
+ from {{ ref('acs_tract_clean') }}
+)
select
census_tract_id
- , acs_tract_raw.year_
- , acs_tract_raw.name_
- , acs_tract_raw.value_
+ , acs_tract.year_
+ , acs_tract.name_
+ , acs_tract.value_
from
- {{ source('minneapolis_old', 'acs_tract_raw') }}
+ acs_tract
inner join census_tracts
using (statefp, countyfp, tractce)
where
- to_date(acs_tract_raw.year_::text , 'YYYY') <@ census_tracts.valid
+ to_date(acs_tract.year_::text , 'YYYY') <@ census_tracts.valid
diff --git a/dbt/models/acs_tract_clean.sql b/dbt/models/acs_tract_clean.sql
new file mode 100644
index 00000000..bd5638a8
--- /dev/null
+++ b/dbt/models/acs_tract_clean.sql
@@ -0,0 +1,20 @@
+with
+acs_tract_raw as (
+ select
+ statefp
+ , countyfp
+ , tractce
+ , year_
+ , name_
+ , value_
+ from {{ source('minneapolis_old', 'acs_tract_raw') }}
+)
+select
+ statefp
+ , countyfp
+ , tractce
+ , year_
+ , name_
+ , case when value_ < 0 then null else value_ end as value_
+from
+ acs_tract_raw
diff --git a/dbt/models/acs_tract_wide.sql b/dbt/models/acs_tract_wide.sql
new file mode 100644
index 00000000..f83da90f
--- /dev/null
+++ b/dbt/models/acs_tract_wide.sql
@@ -0,0 +1,73 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['geoidfq', 'description']}
+ ]
+ )
+}}
+
+{% set years = range(2013, 2023) %}
+
+with acs_tract as (
+ select
+ census_tract_id
+ , year_
+ , name_
+ , value_
+ from {{ ref('acs_tract') }}
+)
+
+, census_tracts as (
+ select
+ census_tract_id
+ , geoidfq
+ from {{ ref("census_tracts") }}
+)
+
+, acs_variables as (
+ select
+ "variable"
+ , description
+ from {{ ref("acs_variables") }}
+)
+
+, acs_tract_extended as (
+ select
+ acs_tract.census_tract_id
+ , census_tracts.geoidfq
+ , acs_tract.year_
+ , acs_tract.name_
+ , acs_tract.value_
+ from
+ acs_tract
+ inner join census_tracts using (census_tract_id)
+)
+
+, distinct_tracts_and_variables as (
+ select distinct
+ geoidfq
+ , name_
+ from acs_tract_extended
+)
+
+select
+ distinct_tracts_and_variables.geoidfq
+ , acs_variables.description
+{% for year_ in years %}
+ , "{{ year_ }}"
+{% endfor %}
+from
+distinct_tracts_and_variables
+inner join acs_variables
+ on distinct_tracts_and_variables.name_ = acs_variables.variable
+{% for year_ in years %}
+left join
+(select
+ geoidfq
+ , name_
+ , value_ as "{{ year_}}"
+from acs_tract_extended
+where year_ = {{ year_ }})
+using (geoidfq, name_)
+{% endfor %}
From 9ea31f2c62db926663c3a64b8b7070fe953926e2 Mon Sep 17 00:00:00 2001
From: Michelangelo Naim
Date: Fri, 16 Aug 2024 17:25:04 -0400
Subject: [PATCH 053/142] adding file to load from bucket to db
---
load_data_server/load_server.py | 365 ++++++++++++++++++++++++++++++++
1 file changed, 365 insertions(+)
create mode 100644 load_data_server/load_server.py
diff --git a/load_data_server/load_server.py b/load_data_server/load_server.py
new file mode 100644
index 00000000..cbf82881
--- /dev/null
+++ b/load_data_server/load_server.py
@@ -0,0 +1,365 @@
+import os
+import re
+from dotenv import load_dotenv
+import subprocess
+import psycopg2
+from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
+import time
+import logging
+from google.cloud import storage
+import argparse
+from tqdm import tqdm
+
+# Load environment variables
+load_dotenv()
+
+# Set up logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+
+# DATA INFO
+PROJECT_NAME = os.getenv('GOOGLE_CLOUD_PROJECT')
+BUCKET_NAME = os.getenv('GOOGLE_CLOUD_BUCKET')
+
+# Paths inside the bucket
+FOLDERS = [
+ 'fair_market_rents',
+]
+
+# DATABASE INFO
+SCHEMA = os.getenv('SCHEMA')
+HOST = os.getenv('HOST')
+DATABASE = os.getenv('DATABASE')
+USERNAME = os.getenv('USERNAME')
+PASSWORD = os.getenv('PASSWORD')
+
+OGR2OGR_OPTS = [
+ "--config", "PG_USE_COPY", "YES",
+ "-progress",
+ "-lco", "PRECISION=NO",
+ "-overwrite",
+ "-lco", "GEOMETRY_NAME=geom",
+ "-nlt", "PROMOTE_TO_MULTI",
+]
+DB_OPTS = [f"PG:dbname={DATABASE} host={HOST} user={USERNAME} password={PASSWORD} port=5432"]
+
+MAX_RETRIES = 3
+RETRY_DELAY = 5 # seconds
+
+def get_db_connection():
+ """Create a database connection with retries."""
+ for attempt in range(MAX_RETRIES):
+ try:
+ conn = psycopg2.connect(
+ host=HOST,
+ database=DATABASE,
+ user=USERNAME,
+ password=PASSWORD
+ )
+ conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
+ return conn
+ except psycopg2.OperationalError as e:
+ if attempt < MAX_RETRIES - 1:
+ logging.warning(f"Connection attempt {attempt + 1} failed. Retrying in {RETRY_DELAY} seconds...")
+ time.sleep(RETRY_DELAY)
+ else:
+ logging.error(f"Failed to connect to the database after {MAX_RETRIES} attempts: {e}")
+ raise
+
+def create_schema_if_not_exists(conn):
+ """Create the schema if it doesn't exist."""
+ with conn.cursor() as cur:
+ cur.execute(f"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = %s);", (SCHEMA,))
+ schema_exists = cur.fetchone()[0]
+
+ if not schema_exists:
+ cur.execute(f"CREATE SCHEMA {SCHEMA};")
+ logging.info(f"Schema '{SCHEMA}' created.")
+ else:
+ logging.info(f"Schema '{SCHEMA}' already exists.")
+
+def generate_table_name(blob_name):
+ """Generate a PostgreSQL-friendly table name from the blob name, including all parent folders and removing duplicates."""
+ table_name = os.path.splitext(blob_name)[0]
+ path_components = table_name.split('/')
+
+ # Remove any leading empty components
+ path_components = [comp for comp in path_components if comp]
+
+ table_name = '_'.join(path_components)
+ table_name = table_name.replace('-', '_').replace('.', '_')
+
+ words = table_name.split('_')
+ unique_words = []
+ for word in words:
+ if word.lower() not in (w.lower() for w in unique_words):
+ unique_words.append(word)
+
+ table_name = '_'.join(unique_words)
+ table_name = re.sub('_+', '_', table_name)
+
+ if table_name[0].isdigit():
+ table_name = 'f_' + table_name
+
+ if len(table_name) > 63:
+ table_name = table_name[:63]
+
+ table_name = table_name.rstrip('_')
+
+ return table_name.lower()
+
+def table_exists(conn, table_name):
+ """Check if a table exists in the specified schema."""
+ with conn.cursor() as cur:
+ cur.execute("""
+ SELECT EXISTS (
+ SELECT FROM information_schema.tables
+ WHERE table_schema = %s AND table_name = %s
+ );
+ """, (SCHEMA, table_name))
+ return cur.fetchone()[0]
+
+def drop_table_if_exists(conn, table_name):
+ """Drop the table if it exists."""
+ with conn.cursor() as cur:
+ cur.execute(f"DROP TABLE IF EXISTS {SCHEMA}.{table_name} CASCADE;")
+
+def load_into_server(conn, file_path, file_type):
+ table_name = os.path.splitext(os.path.basename(file_path))[0]
+ full_table_name = f"{SCHEMA}.{table_name}"
+
+ if table_exists(conn, table_name):
+ drop_table_if_exists(conn, table_name)
+
+ # Upload the file based on its type
+ if file_type == 'shp':
+ upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-nln", full_table_name] + DB_OPTS + [file_path]
+ elif file_type == 'geojson':
+ upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-f", "PostgreSQL"] + DB_OPTS + [file_path, "-nln", full_table_name]
+ else:
+ logging.error(f"Unsupported file type: {file_type}")
+ return False
+
+ for attempt in range(MAX_RETRIES):
+ try:
+ subprocess.check_call(upload_command)
+ logging.info(f"Successfully loaded {file_path} into {full_table_name}")
+ return True
+ except subprocess.CalledProcessError as e:
+ if attempt < MAX_RETRIES - 1:
+ logging.warning(f"Attempt {attempt + 1} failed for {file_path}. Retrying in {RETRY_DELAY} seconds...")
+ time.sleep(RETRY_DELAY)
+ else:
+ logging.error(f"Failed to process {file_path} after {MAX_RETRIES} attempts: {e}")
+ return False
+
+def group_shapefile_components(blobs):
+ """Group Shapefile components together."""
+ shapefile_groups = {}
+ for blob in blobs:
+ name, ext = os.path.splitext(blob.name)
+ if ext.lower() in ['.shp', '.shx', '.dbf', '.prj']:
+ if name not in shapefile_groups:
+ shapefile_groups[name] = []
+ shapefile_groups[name].append(blob)
+ return shapefile_groups
+
+def process_geojson(conn, blob):
+ table_name = generate_table_name(blob.name)
+ if table_exists(conn, table_name):
+ return False # Table already exists, skip processing
+
+ full_table_name = f"{SCHEMA}.{table_name}"
+
+ file_path = os.path.join('/tmp', os.path.basename(blob.name))
+ blob.download_to_filename(file_path)
+
+ upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-f", "PostgreSQL"] + DB_OPTS + [file_path, "-nln", full_table_name]
+
+ success = False
+ for attempt in range(MAX_RETRIES):
+ try:
+ subprocess.check_call(upload_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ success = True
+ break
+ except subprocess.CalledProcessError:
+ if attempt < MAX_RETRIES - 1:
+ time.sleep(RETRY_DELAY)
+
+ os.remove(file_path)
+ return success
+
+def process_shapefile(conn, component_blobs):
+ shp_blob = next(blob for blob in component_blobs if blob.name.endswith('.shp'))
+ table_name = generate_table_name(shp_blob.name)
+
+ if table_exists(conn, table_name):
+ return False # Table already exists, skip processing
+
+ temp_dir = os.path.join('/tmp', table_name)
+ os.makedirs(temp_dir, exist_ok=True)
+
+ for blob in component_blobs:
+ file_ext = os.path.splitext(blob.name)[1]
+ file_name = f"{table_name}{file_ext}"
+ file_path = os.path.join(temp_dir, file_name)
+ blob.download_to_filename(file_path)
+
+ shp_file = f"{table_name}.shp"
+ shp_path = os.path.join(temp_dir, shp_file)
+
+ full_table_name = f"{SCHEMA}.{table_name}"
+
+ upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-nln", full_table_name] + DB_OPTS + [shp_path]
+
+ success = False
+ for attempt in range(MAX_RETRIES):
+ try:
+ subprocess.check_call(upload_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ success = True
+ break
+ except subprocess.CalledProcessError:
+ if attempt < MAX_RETRIES - 1:
+ time.sleep(RETRY_DELAY)
+
+ for file in os.listdir(temp_dir):
+ os.remove(os.path.join(temp_dir, file))
+ os.rmdir(temp_dir)
+
+ return success
+
+def load_csv_into_server(conn, file_path, full_table_name):
+ """Load a CSV file into the PostgreSQL server."""
+ try:
+ with open(file_path, 'r') as f:
+ cursor = conn.cursor()
+ # Read and sanitize the header row
+ header = f.readline().strip().split(',')
+ sanitized_header = [re.sub(r'[^a-zA-Z0-9_]', '_', col.strip('"').strip()) for col in header]
+
+ # Ensure column names are unique
+ seen = set()
+ sanitized_header = [col if col not in seen and not seen.add(col) else f"{col}_dup" for col in sanitized_header]
+
+ create_table_sql = f"""
+ CREATE TABLE {full_table_name} (
+ {','.join([f'"{col}" TEXT' for col in sanitized_header])}
+ );
+ """
+ cursor.execute(create_table_sql)
+
+ # Reset file pointer to beginning
+ f.seek(0)
+
+ # Use COPY to load the data into the table
+ cursor.copy_expert(f"COPY {full_table_name} FROM STDIN WITH CSV HEADER", f)
+ conn.commit()
+ return True
+ except Exception as e:
+ print(f"Error loading CSV into {full_table_name}: {e}")
+ conn.rollback()
+ return False
+
+def process_csv(conn, blob):
+ """Process a CSV file from Google Cloud Storage and load it into the database."""
+ # Generate a table name based on the blob name
+ table_name = generate_table_name(blob.name)
+ full_table_name = f"{SCHEMA}.{table_name}"
+
+ # Check if the table already exists
+ if table_exists(conn, table_name):
+ return False # Table already exists, skip processing
+
+ # Download the CSV file to a temporary location
+ temp_file_name = f"temp_{table_name}.csv"
+ temp_file_path = os.path.join('/tmp', temp_file_name)
+ blob.download_to_filename(temp_file_path)
+
+ try:
+ # Load the CSV into the database
+ success = load_csv_into_server(conn, temp_file_path, full_table_name)
+ return success
+ finally:
+ # Clean up the temporary file
+ if os.path.exists(temp_file_path):
+ os.remove(temp_file_path)
+
+def count_processable_files(blobs):
+ """Count the number of files that will be processed."""
+ count = 0
+ shapefile_groups = group_shapefile_components(blobs)
+ for blob in blobs:
+ if blob.name.endswith('.geojson') or blob.name.endswith('.csv'):
+ count += 1
+ elif blob.name.endswith('.shp'):
+ base_name = os.path.splitext(blob.name)[0]
+ if base_name in shapefile_groups:
+ count += 1
+ return count
+
+def process_file(conn, blob, shapefile_groups, processed_shapefiles):
+ """Process a single file and return whether it was processed."""
+ if blob.name.endswith('.geojson'):
+ return process_geojson(conn, blob)
+ elif blob.name.endswith('.shp'):
+ base_name = os.path.splitext(blob.name)[0]
+ if base_name in shapefile_groups and base_name not in processed_shapefiles:
+ success = process_shapefile(conn, shapefile_groups[base_name])
+ if success:
+ processed_shapefiles.add(base_name)
+ return success
+ elif blob.name.endswith('.csv'):
+ return process_csv(conn, blob)
+ return False
+
+def download_and_process_files(bucket, conn, folder_prefix=''):
+ """Download and process files from the specified folder and its subfolders in the GCS bucket."""
+ blobs = list(bucket.list_blobs(prefix=folder_prefix))
+ total_files = count_processable_files(blobs)
+ shapefile_groups = group_shapefile_components(blobs)
+
+ processed_shapefiles = set()
+
+ with tqdm(total=total_files, desc="Processing files", unit="file") as pbar:
+ for blob in blobs:
+ if blob.name.endswith('/'): # This is a folder
+ continue
+ processed = process_file(conn, blob, shapefile_groups, processed_shapefiles)
+ if processed:
+ pbar.update(1)
+ else:
+ pbar.total -= 1
+ pbar.refresh()
+
+def main(process_entire_bucket=False):
+ try:
+ # Initialize Google Cloud Storage client
+ storage_client = storage.Client(project=PROJECT_NAME)
+ bucket = storage_client.bucket(BUCKET_NAME)
+
+ # Connect to the database
+ conn = get_db_connection()
+ create_schema_if_not_exists(conn)
+
+ if process_entire_bucket:
+ print("Processing entire bucket")
+ download_and_process_files(bucket, conn)
+ else:
+ # Process files in the specified folders
+ for folder in FOLDERS:
+ print(f"Processing folder: {folder}")
+ download_and_process_files(bucket, conn, folder)
+
+ print("Processing completed successfully.")
+
+ except Exception as e:
+ print(f"An error occurred: {e}")
+ finally:
+ if 'conn' in locals() and conn:
+ conn.close()
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Process files from Google Cloud Storage bucket")
+ parser.add_argument('--full-bucket', action='store_true', help='Process the entire bucket instead of specific folders')
+ args = parser.parse_args()
+
+ main(process_entire_bucket=args.full_bucket)
\ No newline at end of file
From 889d8cec21deeb809f91eb8cd3d7deaf27eea6a0 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 16 Aug 2024 18:12:46 -0400
Subject: [PATCH 054/142] filter tracts in wide table using city boundary
---
api/postgrest.conf | 4 +--
api/schema.sql | 25 +++----------------
dbt/models/acs_tract_wide.sql | 15 ++++++++---
dbt/models/census_tracts_in_city_boundary.sql | 18 +++++++++++++
4 files changed, 34 insertions(+), 28 deletions(-)
create mode 100644 dbt/models/census_tracts_in_city_boundary.sql
diff --git a/api/postgrest.conf b/api/postgrest.conf
index 097fba01..ddb71965 100644
--- a/api/postgrest.conf
+++ b/api/postgrest.conf
@@ -93,10 +93,10 @@ openapi-server-proxy-uri = ""
# server-cors-allowed-origins = ""
server-host = "!4"
-server-port = 3000
+server-port = 3001
## Allow getting the request-response timing information through the `Server-Timing` header
-server-timing-enabled = false
+server-timing-enabled = true
## Unix socket location
## if specified it takes precedence over server-port
diff --git a/api/schema.sql b/api/schema.sql
index 7167ade7..8578cdbf 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -2,28 +2,9 @@ drop schema if exists api cascade;
create schema api;
-create view api.parcels as (
- select * from dbt.parcels
-);
-
-create view api.census_tracts as (
- select * from dbt.census_tracts
-);
-
-create view api.census_block_groups as (
- select * from dbt.census_block_groups
-);
-
-create view api.zip_codes as (
- select * from dbt.zip_codes
-);
-
-create view api.emv_in_downtown_west as (
- select dbt.parcels.pin, dbt.parcels.emv_land
- from dbt.parcels
- inner join dbt.neighborhoods
- on st_intersects(st_transform(dbt.parcels.geom, 3857), dbt.neighborhoods.geom)
- where dbt.neighborhoods.name_ = 'Downtown West'
+create view api.acs_tract_wide as (
+ select * from dbt.acs_tract_wide
+ order by random()
);
drop role if exists web_anon;
diff --git a/dbt/models/acs_tract_wide.sql b/dbt/models/acs_tract_wide.sql
index f83da90f..434d2d9e 100644
--- a/dbt/models/acs_tract_wide.sql
+++ b/dbt/models/acs_tract_wide.sql
@@ -2,7 +2,7 @@
config(
materialized='table',
indexes = [
- {'columns': ['geoidfq', 'description']}
+ {'columns': ['description']}
]
)
}}
@@ -18,11 +18,18 @@ with acs_tract as (
from {{ ref('acs_tract') }}
)
+, census_tracts_in_city_boundary as (
+ select
+ census_tract_id
+ from {{ ref("census_tracts_in_city_boundary") }}
+)
+
, census_tracts as (
select
census_tract_id
- , geoidfq
+ , substring(geoidfq from 10) as geoidfq
from {{ ref("census_tracts") }}
+ where census_tract_id in (select census_tract_id from census_tracts_in_city_boundary)
)
, acs_variables as (
@@ -52,8 +59,8 @@ with acs_tract as (
)
select
- distinct_tracts_and_variables.geoidfq
- , acs_variables.description
+ acs_variables.description
+ , distinct_tracts_and_variables.geoidfq as tract_id
{% for year_ in years %}
, "{{ year_ }}"
{% endfor %}
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
new file mode 100644
index 00000000..6f6febbe
--- /dev/null
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -0,0 +1,18 @@
+with census_tracts as (
+ select
+ census_tract_id
+ , geom
+ from {{ ref('census_tracts') }}
+)
+, city_boundary as (
+ select
+ st_transform(geom, 4269) as geom
+ from {{ ref('city_boundary') }}
+)
+select
+ census_tracts.census_tract_id
+from
+ census_tracts
+ , city_boundary
+where st_intersects(census_tracts.geom, city_boundary.geom)
+ and st_area(st_intersection(census_tracts.geom, city_boundary.geom)) / st_area(census_tracts.geom) > 0.2
From ed6c7e6e7ae07d2c4c8486f1fea02f647fe2c19d Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 16:26:15 -0400
Subject: [PATCH 055/142] convert all geometry to standardized srid
---
dbt/dbt_project.yml | 1 +
dbt/models/all_zip_codes_2010.sql | 2 +-
dbt/models/all_zip_codes_2020.sql | 2 +-
dbt/models/census_block_groups.sql | 2 +-
dbt/models/census_tracts.sql | 2 +-
dbt/models/census_tracts_in_city_boundary.sql | 2 +-
dbt/models/city_boundary.sql | 2 +-
dbt/models/commercial_permits.sql | 2 +-
dbt/models/neighborhoods.sql | 2 +-
dbt/models/parcels_base.sql | 2 +-
dbt/models/parcels_to_census_block_groups.sql | 2 +-
dbt/models/parcels_to_zip_codes.sql | 2 +-
dbt/models/parking.sql | 2 +-
dbt/models/parking_to_parcels.sql | 2 +-
dbt/models/residential_permits.sql | 2 +-
dbt/models/zip_codes.sql | 2 +-
16 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/dbt/dbt_project.yml b/dbt/dbt_project.yml
index e4b65a64..34355ccf 100644
--- a/dbt/dbt_project.yml
+++ b/dbt/dbt_project.yml
@@ -24,5 +24,6 @@ clean-targets: # directories to be removed by `dbt clean`
vars:
+ srid: 26915 # use UTM zone 15N for all geometric data. note, this must have meters as the unit of measure
# years for which we have census tract/block group data
census_years: [2010, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]
diff --git a/dbt/models/all_zip_codes_2010.sql b/dbt/models/all_zip_codes_2010.sql
index 8cdafd23..e6f2c5c5 100644
--- a/dbt/models/all_zip_codes_2010.sql
+++ b/dbt/models/all_zip_codes_2010.sql
@@ -1,5 +1,5 @@
select
zcta5ce10 as zip_code,
- geom
+ st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'zip_codes_tl_2020_us_zcta510') }}
diff --git a/dbt/models/all_zip_codes_2020.sql b/dbt/models/all_zip_codes_2020.sql
index aee015ae..9a9a77b0 100644
--- a/dbt/models/all_zip_codes_2020.sql
+++ b/dbt/models/all_zip_codes_2020.sql
@@ -1,4 +1,4 @@
select
zcta5ce20 as zip_code,
- geom
+ st_transform(geom, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'zip_codes_tl_2020_us_zcta520') }}
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index a52c2f47..d3d8ac72 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -37,7 +37,7 @@ census_block_groups as (
, {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
, '[{{ year_ }}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
{% endif %}
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_bg_500k') }}
{% if not loop.last %}union all{% endif %}
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 634e18ac..1119140c 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -14,7 +14,7 @@ select
, {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
, '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
{% endif %}
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_tract_500k') }}
{% if not loop.last %}union all{% endif %}
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
index 6f6febbe..51e1d4e2 100644
--- a/dbt/models/census_tracts_in_city_boundary.sql
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -6,7 +6,7 @@ with census_tracts as (
)
, city_boundary as (
select
- st_transform(geom, 4269) as geom
+ geom
from {{ ref('city_boundary') }}
)
select
diff --git a/dbt/models/city_boundary.sql b/dbt/models/city_boundary.sql
index b34a22ec..1b7fc755 100644
--- a/dbt/models/city_boundary.sql
+++ b/dbt/models/city_boundary.sql
@@ -1,4 +1,4 @@
select
- geom
+ st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'city_boundary_minneapolis') }}
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index 349b6c8e..b51cb23d 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -20,7 +20,7 @@ select
, permit_val as permit_value
, sqf as square_feet
, address
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'commercial_permits_nonresidentialconstruction') }}
where
diff --git a/dbt/models/neighborhoods.sql b/dbt/models/neighborhoods.sql
index b031cf08..bd3da714 100644
--- a/dbt/models/neighborhoods.sql
+++ b/dbt/models/neighborhoods.sql
@@ -1,6 +1,6 @@
select
bdnum as neighborhood_id
, bdname as name_
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'neighborhoods_minneapolis') }}
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 8fb2f2dd..6fb778f1 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -40,5 +40,5 @@ select
, year_built
, sale_date
, sale_value
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from parcels
diff --git a/dbt/models/parcels_to_census_block_groups.sql b/dbt/models/parcels_to_census_block_groups.sql
index 07caa1fb..bb6cc212 100644
--- a/dbt/models/parcels_to_census_block_groups.sql
+++ b/dbt/models/parcels_to_census_block_groups.sql
@@ -13,7 +13,7 @@ parcels as (
select
parcel_id as id
, valid
- , ST_Transform(geom, 4269) as geom
+ , geom
from {{ ref("parcels_base") }}
),
census_block_groups as (
diff --git a/dbt/models/parcels_to_zip_codes.sql b/dbt/models/parcels_to_zip_codes.sql
index 2519888a..6a045300 100644
--- a/dbt/models/parcels_to_zip_codes.sql
+++ b/dbt/models/parcels_to_zip_codes.sql
@@ -13,7 +13,7 @@ parcels as (
select
parcel_id as id
, valid
- , ST_Transform(geom, 4269) as geom
+ , geom
from {{ ref("parcels_base") }}
),
zip_codes as (
diff --git a/dbt/models/parking.sql b/dbt/models/parking.sql
index 6f4e6cdb..cd0b874e 100644
--- a/dbt/models/parking.sql
+++ b/dbt/models/parking.sql
@@ -26,5 +26,5 @@ select
, "housing un" as num_housing_units
, "car parkin" as num_car_parking_spaces
, "bike parki" as num_bike_parking_spaces
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from parking_raw
diff --git a/dbt/models/parking_to_parcels.sql b/dbt/models/parking_to_parcels.sql
index 21c20edc..7eb1c755 100644
--- a/dbt/models/parking_to_parcels.sql
+++ b/dbt/models/parking_to_parcels.sql
@@ -13,7 +13,7 @@ with
select
parking_id as id
, daterange(date_, date_, '[]') as valid
- , ST_Transform(geom, 26915) as geom
+ , geom
from {{ ref('parking') }}
)
, parcels as (
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 95ee9a9d..35a68113 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -27,7 +27,7 @@ select
, permit_val as permit_value
, community_ as community_designation
, notes
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'residential_permits_residentialpermits') }}
where
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 17eac722..346b6b82 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -10,7 +10,7 @@
with city_boundary as (
select
- st_transform(geom, 4269) as geom
+ geom
from
{{ ref('city_boundary') }}
)
From b26754c0a47e5c63439b1133a17718b4019c5744 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 16:26:44 -0400
Subject: [PATCH 056/142] include all zip codes that intersect the city
boundary
---
dbt/models/zip_codes.sql | 1 -
1 file changed, 1 deletion(-)
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 346b6b82..77d9ddd3 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -24,4 +24,3 @@ from
city_boundary
where
st_intersects(all_zip_codes.geom, city_boundary.geom)
- and st_area(st_intersection(all_zip_codes.geom, city_boundary.geom)) / st_area(all_zip_codes.geom) > 0.2
From 8159ba108d15680a5bf23d074d5f62fb4b49cbdc Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 16:26:44 -0400
Subject: [PATCH 057/142] include all zip codes that intersect the city
boundary
---
dbt/models/zip_codes.sql | 1 -
1 file changed, 1 deletion(-)
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 17eac722..6958629e 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -24,4 +24,3 @@ from
city_boundary
where
st_intersects(all_zip_codes.geom, city_boundary.geom)
- and st_area(st_intersection(all_zip_codes.geom, city_boundary.geom)) / st_area(all_zip_codes.geom) > 0.2
From 8c96c52e8acf8820c31f402e380c047fdaff0ce2 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 16:26:15 -0400
Subject: [PATCH 058/142] convert all geometry to standardized srid
---
dbt/dbt_project.yml | 1 +
dbt/models/all_zip_codes_2010.sql | 2 +-
dbt/models/all_zip_codes_2020.sql | 2 +-
dbt/models/census_block_groups.sql | 2 +-
dbt/models/census_tracts.sql | 2 +-
dbt/models/city_boundary.sql | 2 +-
dbt/models/commercial_permits.sql | 2 +-
dbt/models/neighborhoods.sql | 2 +-
dbt/models/parcels_base.sql | 2 +-
dbt/models/parcels_to_census_block_groups.sql | 2 +-
dbt/models/parcels_to_zip_codes.sql | 2 +-
dbt/models/parking.sql | 2 +-
dbt/models/parking_to_parcels.sql | 2 +-
dbt/models/residential_permits.sql | 2 +-
dbt/models/zip_codes.sql | 2 +-
15 files changed, 15 insertions(+), 14 deletions(-)
diff --git a/dbt/dbt_project.yml b/dbt/dbt_project.yml
index e4b65a64..34355ccf 100644
--- a/dbt/dbt_project.yml
+++ b/dbt/dbt_project.yml
@@ -24,5 +24,6 @@ clean-targets: # directories to be removed by `dbt clean`
vars:
+ srid: 26915 # use UTM zone 15N for all geometric data. note, this must have meters as the unit of measure
# years for which we have census tract/block group data
census_years: [2010, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]
diff --git a/dbt/models/all_zip_codes_2010.sql b/dbt/models/all_zip_codes_2010.sql
index 8cdafd23..e6f2c5c5 100644
--- a/dbt/models/all_zip_codes_2010.sql
+++ b/dbt/models/all_zip_codes_2010.sql
@@ -1,5 +1,5 @@
select
zcta5ce10 as zip_code,
- geom
+ st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'zip_codes_tl_2020_us_zcta510') }}
diff --git a/dbt/models/all_zip_codes_2020.sql b/dbt/models/all_zip_codes_2020.sql
index aee015ae..9a9a77b0 100644
--- a/dbt/models/all_zip_codes_2020.sql
+++ b/dbt/models/all_zip_codes_2020.sql
@@ -1,4 +1,4 @@
select
zcta5ce20 as zip_code,
- geom
+ st_transform(geom, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'zip_codes_tl_2020_us_zcta520') }}
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index a52c2f47..d3d8ac72 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -37,7 +37,7 @@ census_block_groups as (
, {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
, '[{{ year_ }}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
{% endif %}
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_bg_500k') }}
{% if not loop.last %}union all{% endif %}
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 634e18ac..1119140c 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -14,7 +14,7 @@ select
, {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
, '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
{% endif %}
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_tract_500k') }}
{% if not loop.last %}union all{% endif %}
diff --git a/dbt/models/city_boundary.sql b/dbt/models/city_boundary.sql
index b34a22ec..1b7fc755 100644
--- a/dbt/models/city_boundary.sql
+++ b/dbt/models/city_boundary.sql
@@ -1,4 +1,4 @@
select
- geom
+ st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'city_boundary_minneapolis') }}
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index 349b6c8e..b51cb23d 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -20,7 +20,7 @@ select
, permit_val as permit_value
, sqf as square_feet
, address
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'commercial_permits_nonresidentialconstruction') }}
where
diff --git a/dbt/models/neighborhoods.sql b/dbt/models/neighborhoods.sql
index b031cf08..bd3da714 100644
--- a/dbt/models/neighborhoods.sql
+++ b/dbt/models/neighborhoods.sql
@@ -1,6 +1,6 @@
select
bdnum as neighborhood_id
, bdname as name_
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'neighborhoods_minneapolis') }}
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 8fb2f2dd..6fb778f1 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -40,5 +40,5 @@ select
, year_built
, sale_date
, sale_value
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from parcels
diff --git a/dbt/models/parcels_to_census_block_groups.sql b/dbt/models/parcels_to_census_block_groups.sql
index 07caa1fb..bb6cc212 100644
--- a/dbt/models/parcels_to_census_block_groups.sql
+++ b/dbt/models/parcels_to_census_block_groups.sql
@@ -13,7 +13,7 @@ parcels as (
select
parcel_id as id
, valid
- , ST_Transform(geom, 4269) as geom
+ , geom
from {{ ref("parcels_base") }}
),
census_block_groups as (
diff --git a/dbt/models/parcels_to_zip_codes.sql b/dbt/models/parcels_to_zip_codes.sql
index 2519888a..6a045300 100644
--- a/dbt/models/parcels_to_zip_codes.sql
+++ b/dbt/models/parcels_to_zip_codes.sql
@@ -13,7 +13,7 @@ parcels as (
select
parcel_id as id
, valid
- , ST_Transform(geom, 4269) as geom
+ , geom
from {{ ref("parcels_base") }}
),
zip_codes as (
diff --git a/dbt/models/parking.sql b/dbt/models/parking.sql
index 6f4e6cdb..cd0b874e 100644
--- a/dbt/models/parking.sql
+++ b/dbt/models/parking.sql
@@ -26,5 +26,5 @@ select
, "housing un" as num_housing_units
, "car parkin" as num_car_parking_spaces
, "bike parki" as num_bike_parking_spaces
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from parking_raw
diff --git a/dbt/models/parking_to_parcels.sql b/dbt/models/parking_to_parcels.sql
index 21c20edc..7eb1c755 100644
--- a/dbt/models/parking_to_parcels.sql
+++ b/dbt/models/parking_to_parcels.sql
@@ -13,7 +13,7 @@ with
select
parking_id as id
, daterange(date_, date_, '[]') as valid
- , ST_Transform(geom, 26915) as geom
+ , geom
from {{ ref('parking') }}
)
, parcels as (
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 95ee9a9d..35a68113 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -27,7 +27,7 @@ select
, permit_val as permit_value
, community_ as community_designation
, notes
- , geom
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'residential_permits_residentialpermits') }}
where
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 6958629e..77d9ddd3 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -10,7 +10,7 @@
with city_boundary as (
select
- st_transform(geom, 4269) as geom
+ geom
from
{{ ref('city_boundary') }}
)
From 98437a0930e3238bea55773f13b2e092dbec57c8 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 16:31:48 -0400
Subject: [PATCH 059/142] add downtown, university, and transit lines
---
dbt/models/downtown.sql | 4 +++
dbt/models/high_frequency_transit_lines.sql | 13 ++++++++++
.../high_frequency_transit_lines_union.sql | 25 +++++++++++++++++++
dbt/models/university.sql | 4 +++
4 files changed, 46 insertions(+)
create mode 100644 dbt/models/downtown.sql
create mode 100644 dbt/models/high_frequency_transit_lines.sql
create mode 100644 dbt/models/high_frequency_transit_lines_union.sql
create mode 100644 dbt/models/university.sql
diff --git a/dbt/models/downtown.sql b/dbt/models/downtown.sql
new file mode 100644
index 00000000..5514d39e
--- /dev/null
+++ b/dbt/models/downtown.sql
@@ -0,0 +1,4 @@
+select
+ st_transform(geom, {{ var("srid") }}) as geom
+from
+ {{ source('minneapolis', 'downtown') }}
diff --git a/dbt/models/high_frequency_transit_lines.sql b/dbt/models/high_frequency_transit_lines.sql
new file mode 100644
index 00000000..459e7400
--- /dev/null
+++ b/dbt/models/high_frequency_transit_lines.sql
@@ -0,0 +1,13 @@
+with lines as (
+ select
+ line_id
+ , year_
+ , geom
+ from {{ ref('high_frequency_transit_lines_union') }}
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['line_id', 'year_']) }} as line_id
+ , year_
+ , geom
+from
+ lines
diff --git a/dbt/models/high_frequency_transit_lines_union.sql b/dbt/models/high_frequency_transit_lines_union.sql
new file mode 100644
index 00000000..8f4eedd2
--- /dev/null
+++ b/dbt/models/high_frequency_transit_lines_union.sql
@@ -0,0 +1,25 @@
+with lines_2015 as (
+ select
+ ogc_fid as line_id,
+ st_transform(geom, {{ var("srid") }}) as geom
+ from
+ {{ source('minneapolis', 'high_frequency_transit_2015_freq_lines') }}
+)
+, lines_2016 as (
+ select
+ ogc_fid as line_id,
+ st_transform(geom, {{ var("srid") }}) as geom
+ from
+ {{ source('minneapolis', 'high_frequency_transit_2016_freq_lines') }}
+)
+select
+ 2015 as year_,
+ line_id,
+ geom
+from lines_2015
+union all
+select
+ 2016 as year_,
+ line_id,
+ geom
+from lines_2016
diff --git a/dbt/models/university.sql b/dbt/models/university.sql
new file mode 100644
index 00000000..6ae78ad1
--- /dev/null
+++ b/dbt/models/university.sql
@@ -0,0 +1,4 @@
+select
+ st_transform(geom, {{ var("srid") }}) as geom
+from
+ {{ source('minneapolis', 'university') }}
From bc1217d3951b6a2e1c9924c4ff7bdf088f4549ef Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 18:00:52 -0400
Subject: [PATCH 060/142] add missing primary keys to allow for qgis viz
---
dbt/models/city_boundary.sql | 3 ++-
dbt/models/downtown.sql | 3 ++-
dbt/models/high_frequency_transit_lines.sql | 17 +++++++++++++----
.../high_frequency_transit_lines_union.sql | 8 ++------
dbt/models/residential_permits.sql | 2 +-
dbt/models/university.sql | 3 ++-
6 files changed, 22 insertions(+), 14 deletions(-)
diff --git a/dbt/models/city_boundary.sql b/dbt/models/city_boundary.sql
index 1b7fc755..88af8782 100644
--- a/dbt/models/city_boundary.sql
+++ b/dbt/models/city_boundary.sql
@@ -1,4 +1,5 @@
select
- st_transform(geom, {{ var("srid") }}) as geom
+ ogc_id as city_boundary_id
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'city_boundary_minneapolis') }}
diff --git a/dbt/models/downtown.sql b/dbt/models/downtown.sql
index 5514d39e..dc3e09cd 100644
--- a/dbt/models/downtown.sql
+++ b/dbt/models/downtown.sql
@@ -1,4 +1,5 @@
select
- st_transform(geom, {{ var("srid") }}) as geom
+ ogc_fid as downtown_id
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'downtown') }}
diff --git a/dbt/models/high_frequency_transit_lines.sql b/dbt/models/high_frequency_transit_lines.sql
index 459e7400..af4c344c 100644
--- a/dbt/models/high_frequency_transit_lines.sql
+++ b/dbt/models/high_frequency_transit_lines.sql
@@ -1,13 +1,22 @@
with lines as (
select
- line_id
- , year_
+ year_
, geom
from {{ ref('high_frequency_transit_lines_union') }}
)
+, stops as (
+ select
+ year_
+ , geom
+ from {{ ref('high_frequency_transit_stops') }}
+)
select
- {{ dbt_utils.generate_surrogate_key(['line_id', 'year_']) }} as line_id
+ year_ as high_frequency_transit_lines_id
, year_
- , geom
+ , lines.geom
+ -- note units are in meters
+ , st_buffer(lines.geom, 106.7) as blue_zone_geom -- 350 feet
+ , st_union(st_buffer(lines.geom, 402.3), st_buffer(stops.geom, 804.7)) as yellow_zone_geom -- quarter mile around lines and half mile around stops
from
lines
+ inner join stops using (year_)
diff --git a/dbt/models/high_frequency_transit_lines_union.sql b/dbt/models/high_frequency_transit_lines_union.sql
index 8f4eedd2..073ec9a1 100644
--- a/dbt/models/high_frequency_transit_lines_union.sql
+++ b/dbt/models/high_frequency_transit_lines_union.sql
@@ -1,25 +1,21 @@
with lines_2015 as (
select
- ogc_fid as line_id,
- st_transform(geom, {{ var("srid") }}) as geom
+ st_union(st_transform(geom, {{ var("srid") }})) as geom
from
{{ source('minneapolis', 'high_frequency_transit_2015_freq_lines') }}
)
, lines_2016 as (
select
- ogc_fid as line_id,
- st_transform(geom, {{ var("srid") }}) as geom
+ st_union(st_transform(geom, {{ var("srid") }})) as geom
from
{{ source('minneapolis', 'high_frequency_transit_2016_freq_lines') }}
)
select
2015 as year_,
- line_id,
geom
from lines_2015
union all
select
2016 as year_,
- line_id,
geom
from lines_2016
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 35a68113..c4fb4267 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -9,7 +9,7 @@
}}
select
- sde_id as residential_permit_id
+ sde_id::int as residential_permit_id
, year::int as year_
, tenure
, housing_ty as housing_type
diff --git a/dbt/models/university.sql b/dbt/models/university.sql
index 6ae78ad1..7c6b4309 100644
--- a/dbt/models/university.sql
+++ b/dbt/models/university.sql
@@ -1,4 +1,5 @@
select
- st_transform(geom, {{ var("srid") }}) as geom
+ ogc_fid as university_id
+ , st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'university') }}
From b177443c0abe39ce78fe1b845f9048d58b2585e4 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 18:01:14 -0400
Subject: [PATCH 061/142] add high frequency transit stops model
---
dbt/models/high_frequency_transit_stops.sql | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 dbt/models/high_frequency_transit_stops.sql
diff --git a/dbt/models/high_frequency_transit_stops.sql b/dbt/models/high_frequency_transit_stops.sql
new file mode 100644
index 00000000..b751153f
--- /dev/null
+++ b/dbt/models/high_frequency_transit_stops.sql
@@ -0,0 +1,21 @@
+with stops_2015 as (
+ select
+ 2015 as year_
+ , st_union(st_transform(geom, {{ var("srid") }}))::geometry(multipoint, {{ var("srid") }}) as geom
+ from {{ source('minneapolis', 'high_frequency_transit_2015_freq_rail_stops') }}
+)
+, stops_2016 as ( -- stops are unchanged in 2016
+ select
+ 2016 as year_
+ , geom
+ from stops_2015
+)
+select
+ year_
+ , geom
+from stops_2015
+union all
+select
+ year_
+ , geom
+from stops_2016
From 190cbebb1161e4ff2b20147c7ebba30dbd51c979 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 19 Aug 2024 18:07:14 -0400
Subject: [PATCH 062/142] remove version 1
---
etl/acs.sql | 27 ----
etl/acs_schema.sql | 50 -------
etl/census_schema.sql | 91 ------------
etl/db.py | 2 -
etl/fair_market_rents.sql | 63 --------
etl/fair_market_rents_schema.sql | 12 --
etl/load_acs_raw.py | 168 ---------------------
etl/load_fair_market_rents_raw.py | 94 ------------
etl/load_parcels.py | 54 -------
etl/load_raw_shapes.py | 90 ------------
etl/load_usps_migration_raw.py | 64 --------
etl/load_zip.py | 22 ---
etl/parcel_schema.sql | 36 -----
etl/parcel_to_bg.sql | 118 ---------------
etl/parcel_to_zip.sql | 118 ---------------
etl/permit_schema.sql | 236 ------------------------------
etl/property_values.sql | 11 --
etl/real_estate_transactions.sql | 73 ---------
etl/segregation.sql | 92 ------------
etl/usps_migration.sql | 156 --------------------
etl/usps_migration_raw_schema.sql | 22 ---
etl/zip_schema.sql | 25 ----
22 files changed, 1624 deletions(-)
delete mode 100644 etl/acs.sql
delete mode 100644 etl/acs_schema.sql
delete mode 100644 etl/census_schema.sql
delete mode 100644 etl/db.py
delete mode 100644 etl/fair_market_rents.sql
delete mode 100644 etl/fair_market_rents_schema.sql
delete mode 100644 etl/load_acs_raw.py
delete mode 100644 etl/load_fair_market_rents_raw.py
delete mode 100644 etl/load_parcels.py
delete mode 100644 etl/load_raw_shapes.py
delete mode 100644 etl/load_usps_migration_raw.py
delete mode 100644 etl/load_zip.py
delete mode 100644 etl/parcel_schema.sql
delete mode 100644 etl/parcel_to_bg.sql
delete mode 100644 etl/parcel_to_zip.sql
delete mode 100644 etl/permit_schema.sql
delete mode 100644 etl/property_values.sql
delete mode 100644 etl/real_estate_transactions.sql
delete mode 100644 etl/segregation.sql
delete mode 100644 etl/usps_migration.sql
delete mode 100644 etl/usps_migration_raw_schema.sql
delete mode 100644 etl/zip_schema.sql
diff --git a/etl/acs.sql b/etl/acs.sql
deleted file mode 100644
index 4026036e..00000000
--- a/etl/acs.sql
+++ /dev/null
@@ -1,27 +0,0 @@
-insert into acs_tract
-select
- id
- , year_
- , name_
- , value_
-from
- acs_tract_raw as t1
- join census_tract as t2 on t1.statefp = t2.statefp
- and t1.countyfp = t2.countyfp
- and t1.tractce = t2.tractce
- and to_date(t1.year_::text , 'YYYY') <@ t2.valid;
-
-insert into acs_bg
-select
- id
- , year_
- , name_
- , value_
-from
- acs_bg_raw as t1
- join census_bg as t2 on t1.statefp = t2.statefp
- and t1.countyfp = t2.countyfp
- and t1.tractce = t2.tractce
- and t1.blkgrpce = t2.blkgrpce
- and to_date(t1.year_::text , 'YYYY') <@ t2.valid;
-
diff --git a/etl/acs_schema.sql b/etl/acs_schema.sql
deleted file mode 100644
index 8acb6088..00000000
--- a/etl/acs_schema.sql
+++ /dev/null
@@ -1,50 +0,0 @@
-drop table if exists acs_variable cascade;
-
-create table acs_variable (
- name_ text primary key
- , description text not null
-);
-
-drop table if exists acs_tract_raw cascade;
-
-create table acs_tract_raw (
- statefp text
- , countyfp text
- , tractce text
- , year_ int
- , name_ text
- , value_ numeric
-);
-
-drop table if exists acs_bg_raw cascade;
-
-create table acs_bg_raw (
- statefp text
- , countyfp text
- , tractce text
- , blkgrpce text
- , year_ int
- , name_ text
- , value_ numeric
-);
-
-drop table if exists acs_tract cascade;
-
-create table acs_tract (
- id int references census_tract (id)
- , year_ int not null
- , name_ text references acs_variable (name_)
- , value_ numeric
- , primary key (id , year_ , name_)
-);
-
-drop table if exists acs_bg cascade;
-
-create table acs_bg (
- id int references census_bg (id)
- , year_ int not null
- , name_ text references acs_variable (name_)
- , value_ numeric
- , primary key (id , year_ , name_)
-);
-
diff --git a/etl/census_schema.sql b/etl/census_schema.sql
deleted file mode 100644
index b90b5a90..00000000
--- a/etl/census_schema.sql
+++ /dev/null
@@ -1,91 +0,0 @@
-drop table if exists census_tract cascade;
-
-create table census_tract (
- id serial primary key
- , statefp text not null
- , countyfp text not null
- , tractce text not null
- , geoidfq text not null
- , valid daterange not null
- , geom geometry(MultiPolygon , 4269) not null
-);
-
-create index census_tract_geom_idx on census_tract using gist (geom);
-
-create index census_tract_valid_idx on census_tract using gist (valid);
-
-insert into census_tract (statefp , countyfp , tractce , geoidfq , valid , geom)
-select
- statefp
- , countyfp
- , tractce
- , affgeoid
- , '[2010-01-01,2020-01-01)'::daterange
- , geom
-from
- cb_2018_27_tract_500k
-union all
-select
- statefp
- , countyfp
- , tractce
- , geoidfq
- , '[2020-01-01,2030-01-01)'::daterange
- , geom
-from
- cb_2023_27_tract_500k;
-
-drop table if exists census_bg cascade;
-
-create table census_bg (
- id serial primary key
- , statefp text not null
- , countyfp text not null
- , tractce text not null
- , blkgrpce text not null
- , geoidfq text not null
- , tract_id int references census_tract (id)
- , valid daterange not null
- , geom geometry(MultiPolygon , 4269) not null
-);
-
-create index census_bg_geom_idx on census_bg using gist (geom);
-
-create index census_bg_valid_idx on census_bg using gist (valid);
-
-insert into census_bg (statefp , countyfp , tractce , blkgrpce , geoidfq , tract_id , valid , geom)
-select
- statefp
- , countyfp
- , tractce
- , blkgrpce
- , bg.geoidfq
- , census_tract.id
- , bg.valid
- , bg.geom
-from (
- select
- statefp
- , countyfp
- , tractce
- , blkgrpce
- , affgeoid as geoidfq
- , '[2010-01-01,2020-01-01)'::daterange as valid
- , geom
- from
- cb_2018_27_bg_500k
- union all
- select
- statefp
- , countyfp
- , tractce
- , blkgrpce
- , geoidfq
- , '[2020-01-01,2030-01-01)'::daterange as valid
- , geom
- from
- cb_2023_27_bg_500k) as bg
- join census_tract using (statefp , countyfp , tractce)
-where
- census_tract.valid && bg.valid;
-
diff --git a/etl/db.py b/etl/db.py
deleted file mode 100644
index acaa0053..00000000
--- a/etl/db.py
+++ /dev/null
@@ -1,2 +0,0 @@
-HOST = "34.123.100.76"
-USER = "postgres"
diff --git a/etl/fair_market_rents.sql b/etl/fair_market_rents.sql
deleted file mode 100644
index d2eb3137..00000000
--- a/etl/fair_market_rents.sql
+++ /dev/null
@@ -1,63 +0,0 @@
-drop table if exists fair_market_rents cascade;
-
-create table fair_market_rents (
- zip_id int references zip_code (id)
- , rent numeric
- , num_bedrooms int
- , year_ int
-);
-
-insert into fair_market_rents (zip_id , rent , num_bedrooms , year_)
-with fmr_zip as (
- select
- zip_code.id as zip_id
- , rent_br0
- , rent_br1
- , rent_br2
- , rent_br3
- , rent_br4
- , year_
- from
- fair_market_rents_raw
- join zip_code on zip_code.zip_code = fair_market_rents_raw.zip
- and zip_code.valid @> to_date(year_::text , 'YYYY'))
- select
- zip_id
- , rent_br0
- , 0
- , year_
- from
- fmr_zip
- union
- select
- zip_id
- , rent_br1
- , 1
- , year_
- from
- fmr_zip
- union
- select
- zip_id
- , rent_br2
- , 2
- , year_
- from
- fmr_zip
- union
- select
- zip_id
- , rent_br3
- , 3
- , year_
- from
- fmr_zip
- union
- select
- zip_id
- , rent_br4
- , 4
- , year_
- from
- fmr_zip;
-
diff --git a/etl/fair_market_rents_schema.sql b/etl/fair_market_rents_schema.sql
deleted file mode 100644
index 4fd2ac52..00000000
--- a/etl/fair_market_rents_schema.sql
+++ /dev/null
@@ -1,12 +0,0 @@
-drop table if exists fair_market_rents_raw cascade;
-
-create table fair_market_rents_raw (
- zip text
- , rent_br0 numeric
- , rent_br1 numeric
- , rent_br2 numeric
- , rent_br3 numeric
- , rent_br4 numeric
- , year_ int
-);
-
diff --git a/etl/load_acs_raw.py b/etl/load_acs_raw.py
deleted file mode 100644
index 4ed52239..00000000
--- a/etl/load_acs_raw.py
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/usr/bin/env python
-
-import logging
-import os
-import psycopg2
-
-from db import HOST, USER
-
-log = logging.getLogger(__name__)
-
-YEAR_RANGE = range(2013, 2023)
-ACS_CODES = {
- "B03002_003E": "population_white_non_hispanic",
- "B03002_004E": "population_black_non_hispanic",
- "B03002_005E": "population_asian_non_hispanic",
- "B03002_006E": "population_native_hawaiian_or_pacific_islander_non_hispanic",
- "B03002_007E": "population_american_indian_or_alaska_native_non_hispanic",
- "B03002_008E": "population_other_non_hispanic",
- "B03002_009E": "population_multiple_races_non_hispanic",
- "B03002_010E": "population_multiple_races_and_other_non_hispanic",
- "B07204_001E": "geographic_mobility_total_responses",
- "B07204_002E": "geographic_mobility_same_house_1_year_ago",
- "B07204_004E": "geographic_mobility_different_house_1_year_ago_same_city",
- "B07204_005E": "geographic_mobility_different_house_1_year_ago_same_county",
- "B07204_006E": "geographic_mobility_different_house_1_year_ago_same_state",
- "B07204_007E": "geographic_mobility_different_house_1_year_ago_same_country",
- "B07204_016E": "geographic_mobility_different_house_1_year_ago_abroad",
- "B01003_001E": "population",
- "B02001_002E": "white",
- "B02001_003E": "black",
- "B02001_004E": "american_indian_or_alaska_native",
- "B02001_005E": "asian",
- "B02001_006E": "native_hawaiian_or_pacific_islander",
- "B03001_003E": "population_hispanic_or_latino",
- "B02001_007E": "other_race",
- "B02001_008E": "multiple_races",
- "B02001_009E": "multiple_races_and_other_race",
- "B02001_010E": "two_or_more_races_excluding_other",
- "B02015_002E": "east_asian_chinese",
- "B02015_003E": "east_asian_hmong",
- "B02015_004E": "east_asian_japanese",
- "B02015_005E": "east_asian_korean",
- "B02015_006E": "east_asian_mongolian",
- "B02015_007E": "east_asian_okinawan",
- "B02015_008E": "east_asian_taiwanese",
- "B02015_009E": "east_asian_other",
- "B02015_010E": "southeast_asian_burmese",
- "B02015_011E": "southeast_asian_cambodian",
- "B02015_012E": "southeast_asian_filipino",
- "B02015_013E": "southeast_asian_indonesian",
- "B02015_014E": "southeast_asian_laotian",
- "B02015_015E": "southeast_asian_malaysian",
- "B02015_016E": "southeast_asian_mien",
- "B02015_017E": "southeast_asian_singaporean",
- "B02015_018E": "southeast_asian_thai",
- "B02015_019E": "southeast_asian_viet",
- "B02015_020E": "southeast_asian_other",
- "B02015_021E": "south_asian_asian_indian",
- "B02015_022E": "south_asian_bangladeshi",
- "B02015_023E": "south_asian_bhutanese",
- "B02015_024E": "south_asian_nepalese",
- "B02015_025E": "south_asian_pakistani",
- "B02015_026E": "south_asian_sikh",
- "B02015_027E": "south_asian_sri_lankan",
- "B02015_028E": "south_asian_other",
- "B02015_029E": "central_asian_kazakh",
- "B02015_030E": "central_asian_uzbek",
- "B02015_031E": "central_asian_other",
- "B02015_032E": "other_asian_specified",
- "B02015_033E": "other_asian_not_specified",
- "B19013_001E": "median_household_income",
- "B19013A_001E": "median_household_income_white",
- "B19013H_001E": "median_household_income_white_non_hispanic",
- "B19013I_001E": "median_household_income_hispanic",
- "B19013B_001E": "median_household_income_black",
- "B19013C_001E": "median_household_income_american_indian_or_alaska_native",
- "B19013D_001E": "median_household_income_asian",
- "B19013E_001E": "median_household_income_native_hawaiian_or_pacific_islander",
- "B19013F_001E": "median_household_income_other_race",
- "B19013G_001E": "median_household_income_multiple_races",
- "B19019_002E": "median_household_income_1_person_households",
- "B19019_003E": "median_household_income_2_person_households",
- "B19019_004E": "median_household_income_3_person_households",
- "B19019_005E": "median_household_income_4_person_households",
- "B19019_006E": "median_household_income_5_person_households",
- "B19019_007E": "median_household_income_6_person_households",
- "B19019_008E": "median_household_income_7_or_more_person_households",
- "B01002_001E": "median_age",
- "B01002_002E": "median_age_male",
- "B01002_003E": "median_age_female",
- "B25031_001E": "median_gross_rent",
- "B25031_002E": "median_gross_rent_0_bedrooms",
- "B25031_003E": "median_gross_rent_1_bedrooms",
- "B25031_004E": "median_gross_rent_2_bedrooms",
- "B25031_005E": "median_gross_rent_3_bedrooms",
- "B25031_006E": "median_gross_rent_4_bedrooms",
- "B25031_007E": "median_gross_rent_5_bedrooms",
- "B25032_001E": "total_housing_units",
- "B25032_002E": "total_owner_occupied_housing_units",
- "B25032_013E": "total_renter_occupied_housing_units",
- "B25070_001E": "median_gross_rent_as_percentage_of_household_income",
-}
-
-
-def main():
- conn = psycopg2.connect(host=HOST, user=USER, database="cities")
- cur = conn.cursor()
-
- with open("etl/acs_schema.sql", "r") as f:
- cur.execute(f.read())
-
- for code, desc in ACS_CODES.items():
- cur.execute("insert into acs_variable values (%s, %s)", (code, desc))
- conn.commit()
-
- cur.execute("drop table if exists acs_tract_temp")
- cur.execute(
- "create temp table acs_tract_temp (statefp text, countyfp text, tractce text, value numeric)"
- )
-
- for code in ACS_CODES.keys():
- desc = ACS_CODES[code]
- for year in YEAR_RANGE:
- log.info(f"Loading {desc} for {year}")
- filename = f"zoning/data/raw/demographics/tracts/{desc}/{year}.csv"
- if not os.path.isfile(filename):
- logging.info(f"File {filename} does not exist")
- continue
-
- cur.execute("truncate acs_tract_temp")
-
- with open(filename, "r") as f:
- cur.copy_expert("copy acs_tract_temp from stdin with csv header", f)
-
- cur.execute(
- "insert into acs_tract_raw select statefp, countyfp, tractce, %s, %s, value from acs_tract_temp",
- (year, code),
- )
- conn.commit()
-
- cur.execute("drop table if exists acs_bg_temp")
- cur.execute(
- "create temp table acs_bg_temp (statefp text, countyfp text, tractce text, blkgrpce text, value numeric)"
- )
-
- for code in ACS_CODES.keys():
- desc = ACS_CODES[code]
- for year in YEAR_RANGE:
- log.info(f"Loading {desc} for {year}")
- filename = f"zoning/data/raw/demographics/block_groups/{desc}/{year}.csv"
- if not os.path.isfile(filename):
- logging.info(f"File {filename} does not exist")
- continue
-
- cur.execute("truncate acs_bg_temp")
-
- with open(filename, "r") as f:
- cur.copy_expert("copy acs_bg_temp from stdin with csv header", f)
- cur.execute(
- "insert into acs_bg_raw select statefp, countyfp, tractce, blkgrpce, %s, %s, value from acs_bg_temp",
- (year, code),
- )
- conn.commit()
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.INFO)
- main()
diff --git a/etl/load_fair_market_rents_raw.py b/etl/load_fair_market_rents_raw.py
deleted file mode 100644
index 565c8a05..00000000
--- a/etl/load_fair_market_rents_raw.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/usr/bin/env python
-
-import logging
-import os
-import glob
-from io import StringIO
-
-import psycopg2
-import pandas as pd
-
-from db import HOST, USER
-
-log = logging.getLogger(__name__)
-
-RAW_DATA_DIRECTORY = "zoning/data/raw/demographics/zip_codes/fair_market_rents"
-
-
-def preprocess_csv_to_df(filename):
- year = filename.split("_")[-1].replace(".csv", "")
-
- df = pd.read_csv(filename, dtype=str, na_values={})
-
- rename_dict = {}
- for col in list(df.columns):
- if "zip" in col.lower() or col == "zcta":
- rename_dict[col] = "zip_code"
- elif "BR" in col and "90" not in col and "110" not in col:
- rename_dict[col] = "rent_br" + col.lower().split("br")[0][-1]
- elif "area_rent_br" in col:
- rename_dict[col] = "rent_br" + col[-1]
- elif "safmr" in col and "90" not in col and "110" not in col:
- rename_dict[col] = "rent_br" + col.split("_")[-1][0]
-
- df = df.rename(columns=rename_dict)[
- [
- "zip_code",
- "rent_br0",
- "rent_br1",
- "rent_br2",
- "rent_br3",
- "rent_br4",
- ]
- ]
-
- for col in df.columns:
- if "rent_" in col:
- df[col] = [x.replace("$", "").replace(",", "") for x in df[col]]
-
- return (year, df)
-
-
-def copy_from_stringio(cur, df, table):
- """Here we are going save the dataframe in memory and use copy_from() to copy it to the table"""
- buf = StringIO()
- df.to_csv(buf, index=False, header=False)
- buf.seek(0)
- cur.copy_from(buf, table, sep=",")
-
-
-def main():
- conn = psycopg2.connect(host=HOST, user=USER, database="cities")
- cur = conn.cursor()
-
- with open("etl/fair_market_rents_schema.sql", "r") as f:
- cur.execute(f.read())
-
- cur.execute("drop table if exists fmr_temp")
- cur.execute(
- """
- create temp table fmr_temp (
- zip text
- , rent_br0 numeric
- , rent_br1 numeric
- , rent_br2 numeric
- , rent_br3 numeric
- , rent_br4 numeric)
- """
- )
-
- for filename in glob.glob(f"{RAW_DATA_DIRECTORY}/*.csv"):
- (year, df) = preprocess_csv_to_df(filename)
- cur.execute("truncate fmr_temp")
- copy_from_stringio(cur, df, "fmr_temp")
-
- cur.execute(
- "insert into fair_market_rents_raw select *, %s as year from fmr_temp",
- (year,),
- )
- conn.commit()
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.INFO)
- main()
diff --git a/etl/load_parcels.py b/etl/load_parcels.py
deleted file mode 100644
index 7a114b34..00000000
--- a/etl/load_parcels.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env python
-
-import logging
-import psycopg2
-
-from db import HOST, USER
-
-log = logging.getLogger(__name__)
-
-PARCEL_YEARS = range(2002, 2024)
-COUNTY_ID = "053"
-
-
-def main():
- conn = psycopg2.connect(host=HOST, user=USER, database="cities")
- cur = conn.cursor()
-
- with open("etl/parcel_schema.sql", "r") as f:
- cur.execute(f.read())
- conn.commit()
-
- # select distinct geometry from all parcel tables
- distinct_geom = " union ".join(
- f"select geom from parcel_raw_{year} where upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'"
- for year in PARCEL_YEARS
- )
- parcel_geom_load = f"insert into parcel_geom (geom) {distinct_geom};"
- log.info("Executing: %s", parcel_geom_load)
- cur.execute(parcel_geom_load)
- conn.commit()
-
- # insert parcel data into parcel table
- parcel_data = " union all ".join(
- f"""
- select replace(pin, '{COUNTY_ID}-', ''), '[{year-1}-01-01,{year}-01-01)'::daterange, nullif(emv_land, 0), nullif(emv_bldg, 0), nullif(emv_total, 0), nullif(year_built, 0), sale_date, nullif(sale_value, 0), parcel_geom.id
- from parcel_raw_{year}, parcel_geom
- where parcel_raw_{year}.geom = parcel_geom.geom
- and upper({'city' if year < 2018 else 'ctu_name'}) = 'MINNEAPOLIS'
- """
- for year in PARCEL_YEARS
- )
-
- parcel_load = f"""
- insert into parcel (pid, valid, emv_land, emv_building, emv_total, year_built, sale_date, sale_value, geom_id)
- {parcel_data}
- """
- log.info("Executing: %s", parcel_load)
- cur.execute(parcel_load)
- conn.commit()
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.INFO)
- main()
diff --git a/etl/load_raw_shapes.py b/etl/load_raw_shapes.py
deleted file mode 100644
index 41119a47..00000000
--- a/etl/load_raw_shapes.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python
-
-import glob
-import subprocess
-import logging
-import os
-
-from db import HOST, USER
-
-log = logging.getLogger(__name__)
-
-BASE_DIR = "zoning/data/raw"
-OGR2OGR_OPTS = [
- "--config",
- "PG_USE_COPY", # use postgres specific copy
- "-progress",
- "-lco",
- "PRECISION=NO", # disable use of numeric types (required when shapefiles mis-specify numeric precision)
- "-overwrite", # overwrite existing tables
- "-lco",
- "GEOMETRY_NAME=geom", # name of geometry column
- "-nlt",
- "PROMOTE_TO_MULTI", # promote all POLYGONs to MULTIPOLYGONs
-]
-DB_OPTS = [f"Pg:dbname=cities host={HOST} user={USER} port=5432"]
-
-# (shapefile, table_name) pairs. shapefiles are relative to BASE_DIR
-REL_SHAPES = [
- (
- "base/shp_society_census2000tiger_zcta/Census2000TigerZipCodeTabAreas.shp",
- "zip_raw_2000",
- ),
- (
- "base/shp_bdry_zip_code_tabulation_areas/zip_code_tabulation_areas.shp",
- "zip_raw_2020",
- ),
- (
- "base/hennepin_county_census_tracts_2018/cb_2018_27_tract_500k.shp",
- "census_tract_raw_2018",
- ),
- (
- "base/hennepin_county_census_block_groups_2018/cb_2018_27_bg_500k.shp",
- "census_block_group_raw_2018",
- ),
- (
- "base/hennepin_county_census_tracts_2023/cb_2023_27_tract_500k.shp",
- "census_tract_raw_2023",
- ),
- (
- "base/hennepin_county_census_block_groups_2023/cb_2023_27_bg_500k.shp",
- "census_block_group_raw_2023",
- ),
- (
- "commercial_permits/shp_struc_non_res_construction/NonresidentialConstruction.shp",
- "commercial_permits_raw",
- ),
- (
- "residential_permits/shp_econ_residential_building_permts/ResidentialPermits.shp",
- "residential_permits_raw",
- ),
-]
-
-
-def main():
- # convert relative paths to absolute paths
- abs_shapes = [(os.path.join(BASE_DIR, shape), table) for shape, table in REL_SHAPES]
-
- for parcel_shape_dir in glob.glob(
- os.path.join(BASE_DIR, "property_values/shp_plan_regional_parcels_*/")
- ):
- year = int(parcel_shape_dir.split("/")[-2].split("_")[-1])
- shape = os.path.join(parcel_shape_dir, f"Parcels{year}Hennepin.shp")
- table = f"parcel_raw_{year}"
- abs_shapes.append((shape, table))
-
- for shape, table in abs_shapes:
- if os.path.exists(shape):
- log.info("Loading %s into %s", shape, table)
- else:
- log.warn("Skipping %s because it does not exist", shape)
- continue
-
- subprocess.check_call(
- ["ogr2ogr"] + OGR2OGR_OPTS + ["-nln", table] + DB_OPTS + [shape]
- )
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.INFO)
- main()
diff --git a/etl/load_usps_migration_raw.py b/etl/load_usps_migration_raw.py
deleted file mode 100644
index c05f8e0b..00000000
--- a/etl/load_usps_migration_raw.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python
-
-import glob
-import logging
-import psycopg2
-
-from db import HOST, USER
-
-log = logging.getLogger(__name__)
-
-
-RAW_DATA_DIRECTORY = "zoning/data/raw/demographics/zip_codes/usps_migration"
-
-
-def main():
- conn = psycopg2.connect(host=HOST, user=USER, database="cities")
- cur = conn.cursor()
-
- with open("etl/usps_migration_raw_schema.sql", "r") as f:
- cur.execute(f.read())
-
- cur.execute("drop table if exists m_temp")
- cur.execute(
- """
- create temp table m_temp (
- yyyymm text
- , zip_code text
- , city text
- , state text
- , total_from_zip numeric
- , total_from_zip_business numeric
- , total_from_zip_family numeric
- , total_from_zip_individual numeric
- , total_from_zip_perm numeric
- , total_from_zip_temp numeric
- , total_to_zip numeric
- , total_to_zip_business numeric
- , total_to_zip_family numeric
- , total_to_zip_individual numeric
- , total_to_zip_perm numeric
- , total_to_zip_temp numeric
- )
- """
- )
-
- for filename in glob.glob(f"{RAW_DATA_DIRECTORY}/*.csv"):
- log.info(f"Loading {filename}")
- year = filename.split("/")[-1].split(".")[0].replace("Y", "")
-
- cur.execute("truncate m_temp")
-
- with open(filename, "r") as f:
- cur.copy_expert("copy m_temp from stdin with csv header", f)
-
- cur.execute(
- "insert into usps_migration_raw select *, %s from m_temp",
- (year,),
- )
- conn.commit()
-
-
-if __name__ == "__main__":
- logging.basicConfig(level=logging.INFO)
- main()
diff --git a/etl/load_zip.py b/etl/load_zip.py
deleted file mode 100644
index 02a27cbc..00000000
--- a/etl/load_zip.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import psycopg2
-
-from db import HOST, USER
-
-PARCEL_YEARS = range(2002, 2024)
-COUNTY_ID = "053"
-
-conn = psycopg2.connect(host=HOST, user=USER, database="cities")
-cur = conn.cursor()
-
-with open("etl/zip_schema.sql", "r") as f:
- cur.execute(f.read())
-conn.commit()
-
-zip_load = """
-insert into zip_code(zip_code, valid, geom)
-select zcta5ce20, '[2020-01-01,)'::daterange, geom from zip_raw_2020
-union select zcta, '[2000-01-01,2020-01-01)'::daterange, ST_Transform(geom, 4269) from zip_raw_2000
-"""
-print("Executing:", zip_load)
-cur.execute(zip_load)
-conn.commit()
diff --git a/etl/parcel_schema.sql b/etl/parcel_schema.sql
deleted file mode 100644
index 24bf8523..00000000
--- a/etl/parcel_schema.sql
+++ /dev/null
@@ -1,36 +0,0 @@
-create extension if not exists postgis;
-
-drop table if exists parcel_geom cascade;
-
-create table parcel_geom (
- id serial primary key
- , geom geometry(MultiPolygon , 26915) not null
-);
-
-create index parcel_geom_idx on parcel_geom using gist (geom);
-
-drop table if exists parcel cascade;
-
-create table parcel (
- id serial primary key
- , pid text not null
- , valid daterange not null
- , emv_land numeric
- , emv_building numeric
- , emv_total numeric
- , year_built int
- , sale_date date
- , sale_value numeric
- , geom_id int references parcel_geom (id)
-);
-
-comment on column parcel.valid is 'Dates for which this parcel is valid';
-
-comment on column parcel.pid is 'Municipal parcel ID';
-
-comment on column parcel.emv_land is 'Estimated Market Value, land';
-
-comment on column parcel.emv_building is 'Estimated Market Value, buildings';
-
-comment on column parcel.emv_total is 'Estimated Market Value, total (may be more than sum of land and building)';
-
diff --git a/etl/parcel_to_bg.sql b/etl/parcel_to_bg.sql
deleted file mode 100644
index 5dbc7ac9..00000000
--- a/etl/parcel_to_bg.sql
+++ /dev/null
@@ -1,118 +0,0 @@
-drop type if exists parcel_census_bg_type cascade;
-
-create type parcel_census_bg_type as enum (
- 'within'
- , 'most_overlap'
- , 'closest'
-);
-
-drop table if exists parcel_census_bg;
-
-create table parcel_census_bg (
- parcel_id int references parcel (id)
- , census_bg_id int references census_bg (id)
- , valid daterange not null
- , type parcel_census_bg_type not null
-);
-
-with parcel_with_geom as (
- select
- parcel.id
- , geom_id
- , valid
- , ST_Transform (geom
- , 4269) as geom
- from
- parcel
- join parcel_geom on geom_id = parcel_geom.id
-)
-, parcel_within as (
- -- easy case: one parcel in one bg
- select
- parcel.id as parcel_id
- , census_bg.id as census_bg_id
- , parcel.valid * census_bg.valid as valid
- from
- parcel_with_geom as parcel
- join census_bg on ST_Within (parcel.geom
- , census_bg.geom)
- and parcel.valid && census_bg.valid
-)
-, parcel_not_within as (
- -- parcels that are not fully within any bg
- select
- *
- from
- parcel_with_geom
- where
- not exists (
- select
- parcel_id
- from
- parcel_within
- where
- parcel_id = id)
-)
-, parcel_largest_overlap as (
- -- parcels that overlap multiple bgs map to the one with the largest overlap
- select distinct on (parcel.id)
- parcel.id as parcel_id
- , census_bg.id as census_bg_id
- , parcel.valid * census_bg.valid as valid
- from
- parcel_not_within as parcel
- join census_bg on ST_Intersects (parcel.geom
- , census_bg.geom)
- and parcel.valid && census_bg.valid
- order by
- parcel_id
- , ST_Area (ST_Intersection (parcel.geom
- , census_bg.geom)) desc
-)
-, parcel_no_overlap as (
- -- parcels that do not overlap any bg
- select
- *
- from
- parcel_not_within
- where
- not exists (
- select
- parcel_id
- from
- parcel_largest_overlap
- where
- parcel_id = id)
-)
-, parcel_closest as (
- -- parcels that overlap no bgs map to the closest one
- select distinct on (parcel.id)
- parcel.id as parcel_id
- , census_bg.id as census_bg_id
- , parcel.valid * census_bg.valid as valid
- from
- parcel_no_overlap as parcel
- join census_bg on parcel.valid && census_bg.valid
- order by
- parcel_id
- , ST_Distance (parcel.geom
- , census_bg.geom))
- insert into parcel_census_bg
- select
- *
- , 'within'::parcel_census_bg_type
- from
- parcel_within
- union all
- select
- *
- , 'most_overlap'::parcel_census_bg_type
- from
- parcel_largest_overlap
- union all
- select
- *
- , 'closest'::parcel_census_bg_type
- from
- parcel_closest;
-
diff --git a/etl/parcel_to_zip.sql b/etl/parcel_to_zip.sql
deleted file mode 100644
index 669e48a1..00000000
--- a/etl/parcel_to_zip.sql
+++ /dev/null
@@ -1,118 +0,0 @@
-drop type if exists parcel_zip_type;
-
-create type parcel_zip_type as enum (
- 'within'
- , 'most_overlap'
- , 'closest'
-);
-
-drop table if exists parcel_zip;
-
-create table parcel_zip (
- parcel_id int references parcel (id)
- , zip_code_id int references zip_code (id)
- , valid daterange not null
- , type parcel_zip_type not null
-);
-
-with parcel_with_geom as (
- select
- parcel.id
- , geom_id
- , valid
- , ST_Transform (geom
- , 4269) as geom
- from
- parcel
- join parcel_geom on geom_id = parcel_geom.id
-)
-, parcel_in_zip as (
- -- easy case: one parcel in one zip code
- select
- parcel.id as parcel_id
- , zip_code.id as zip_code_id
- , parcel.valid * zip_code.valid as valid
- from
- parcel_with_geom as parcel
- join zip_code on ST_Within (parcel.geom
- , zip_code.geom)
- and parcel.valid && zip_code.valid
-)
-, parcel_not_within_zip as (
- -- parcels that are not fully within any zip code
- select
- *
- from
- parcel_with_geom
- where
- not exists (
- select
- parcel_id
- from
- parcel_in_zip
- where
- parcel_id = id)
-)
-, parcel_largest_overlap as (
- -- parcels that overlap multiple zip codes map to the one with the largest overlap
- select distinct on (parcel.id)
- parcel.id as parcel_id
- , zip_code.id as zip_code_id
- , parcel.valid * zip_code.valid as valid
- from
- parcel_not_within_zip as parcel
- join zip_code on ST_Intersects (parcel.geom
- , zip_code.geom)
- and parcel.valid && zip_code.valid
- order by
- parcel_id
- , ST_Area (ST_Intersection (parcel.geom
- , zip_code.geom)) desc
-)
-, parcel_no_overlap as (
- -- parcels that do not overlap any zip code
- select
- *
- from
- parcel_not_within_zip
- where
- not exists (
- select
- parcel_id
- from
- parcel_largest_overlap
- where
- parcel_id = id)
-)
-, parcel_closest as (
- -- parcels that overlap no zip codes map to the closest one
- select distinct on (parcel.id)
- parcel.id as parcel_id
- , zip_code.id as zip_code_id
- , parcel.valid * zip_code.valid as valid
- from
- parcel_no_overlap as parcel
- join zip_code on parcel.valid && zip_code.valid
- order by
- parcel_id
- , ST_Distance (parcel.geom
- , zip_code.geom))
- insert into parcel_zip
- select
- *
- , 'within'::parcel_zip_type
- from
- parcel_in_zip
- union all
- select
- *
- , 'most_overlap'::parcel_zip_type
- from
- parcel_largest_overlap
- union all
- select
- *
- , 'closest'::parcel_zip_type
- from
- parcel_closest;
-
diff --git a/etl/permit_schema.sql b/etl/permit_schema.sql
deleted file mode 100644
index c5c7e5e9..00000000
--- a/etl/permit_schema.sql
+++ /dev/null
@@ -1,236 +0,0 @@
-drop table if exists residential_permit cascade;
-
-create table residential_permit (
- id serial primary key
- , ctu_id text
- , coctu_id text
- , year int
- , tenure text
- , housing_ty text
- , res_permit text
- , address text
- , zip_code text
- , name text
- , buildings int
- , units int
- , age_restri int
- , memory_car int
- , assisted int
- , com_off_re boolean
- , sqf numeric
- , public_fun boolean
- , permit_val numeric
- , community_ text
- , notes text
- , pin text
- , geom geometry(multipoint , 26915)
-);
-
-create index residential_permit_geom_idx on residential_permit using gist (geom);
-
-insert into residential_permit (ctu_id , coctu_id , year , tenure , housing_ty , res_permit , address , zip_code , name , buildings , units , age_restri , memory_car , assisted , com_off_re , sqf , public_fun , permit_val , community_ , notes , pin , geom)
-select
- ctu_id
- , coctu_id
- , year::int
- , tenure
- , housing_ty
- , res_permit
- , address
- , zip_code
- , name
- , buildings
- , units
- , age_restri
- , memory_car
- , assisted
- , com_off_re = 'Y'
- , sqf
- , public_fun = 'Y'
- , permit_val
- , community_
- , notes
- , pin
- , geom
-from
- residential_permits_raw
-where
- co_code = '053'
- and lower(ctu_name) = 'minneapolis';
-
-drop table if exists residential_permit_parcel;
-
-create table residential_permit_parcel (
- permit_id int references residential_permit (id)
- , parcel_id int references parcel (id)
- , type_ region_tag_type
-);
-
-with within as (
- select
- residential_permit.id as permit_id
- , parcel.id as parcel_id
- from
- parcel_with_geom as parcel
- join residential_permit on st_within (residential_permit.geom
- , parcel.geom)
- and to_date(year::text
- , 'YYYY') <@ parcel.valid
-)
-, not_within as (
- select
- id
- , year
- , geom
- from
- residential_permit
- where
- not exists (
- select
- permit_id
- from
- within
- where
- permit_id = id)
-)
-, closest as (
- select distinct on (permit.id)
- permit.id as permit_id
- , parcel.id as parcel_id
- from
- not_within as permit
- join parcel_with_geom as parcel on st_dwithin (permit.geom
- , parcel.geom
- , 100.0)
- and to_date(year::text
- , 'YYYY') <@ parcel.valid
- order by
- permit_id
- , st_distance (permit.geom
- , parcel.geom))
- insert into residential_permit_parcel
- select
- permit_id
- , parcel_id
- , 'within'::region_tag_type
- from
- within
- union all
- select
- permit_id
- , parcel_id
- , 'closest'::region_tag_type
-from
- closest;
-
-drop table if exists commercial_permit cascade;
-
-create table commercial_permit (
- id serial primary key
- , ctu_id text
- , coctu_id text
- , year int
- , nonres_gro text
- , nonres_sub text
- , nonres_typ text
- , bldg_name text
- , bldg_desc text
- , permit_typ text
- , permit_val numeric
- , sqf int
- , address text
- , zip_code text
- , pin text
- , geom geometry(multipoint , 26915)
-);
-
-create index commercial_permit_geom_idx on commercial_permit using gist (geom);
-
-insert into commercial_permit (ctu_id , coctu_id , year , nonres_gro , nonres_sub , nonres_typ , bldg_name , bldg_desc , permit_typ , permit_val , sqf , address , zip_code , pin , geom)
-select
- ctu_id
- , coctu_id
- , year::int
- , nonres_gro
- , nonres_sub
- , nonres_typ
- , bldg_name
- , bldg_desc
- , permit_typ
- , permit_val
- , sqf
- , address
- , zip_code
- , pin
- , geom
-from
- commercial_permits_raw
-where
- co_code = '053'
- and lower(ctu_name) = 'minneapolis';
-
-drop table if exists commercial_permit_parcel;
-
-create table commercial_permit_parcel (
- permit_id int references commercial_permit (id)
- , parcel_id int references parcel (id)
- , type_ region_tag_type
-);
-
-with within as (
- select
- commercial_permit.id as permit_id
- , parcel.id as parcel_id
- from
- parcel_with_geom as parcel
- join commercial_permit on st_within (commercial_permit.geom
- , parcel.geom)
- and to_date(year::text
- , 'YYYY') <@ parcel.valid
-)
-, not_within as (
- select
- id
- , year
- , geom
- from
- commercial_permit
- where
- not exists (
- select
- permit_id
- from
- within
- where
- permit_id = id)
-)
-, closest as (
- select distinct on (permit.id)
- permit.id as permit_id
- , parcel.id as parcel_id
- from
- not_within as permit
- join parcel_with_geom as parcel on st_dwithin (permit.geom
- , parcel.geom
- , 100.0)
- and to_date(year::text
- , 'YYYY') <@ parcel.valid
- order by
- permit_id
- , st_distance (permit.geom
- , parcel.geom))
- insert into commercial_permit_parcel
- select
- permit_id
- , parcel_id
- , 'within'::region_tag_type
- from
- within
- union all
- select
- permit_id
- , parcel_id
- , 'closest'::region_tag_type
-from
- closest;
-
diff --git a/etl/property_values.sql b/etl/property_values.sql
deleted file mode 100644
index 3e163f9c..00000000
--- a/etl/property_values.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-drop view if exists property_values;
-
-create view property_values as (
- select
- id
- , pid
- , emv_total as value_
- , valid
- from
- parcel);
-
diff --git a/etl/real_estate_transactions.sql b/etl/real_estate_transactions.sql
deleted file mode 100644
index 6980b5ed..00000000
--- a/etl/real_estate_transactions.sql
+++ /dev/null
@@ -1,73 +0,0 @@
-drop table if exists real_estate_transactions_scraped;
-
-create table real_estate_transactions_scraped (
- parcel_id text
- , address text
- , sale_date date
- , sale_price numeric
- , building_area numeric
- , beds numeric
- , baths numeric
- , stories numeric
- , year_built numeric
- , neighborhood text
- , property_type text
-);
-
-\copy real_estate_transactions_scraped from 'zoning/data/processed/real_estate_transactions/real_estate_transactions.csv' with csv header delimiter ',';
-drop table if exists real_estate_transactions_raw;
-
-create table real_estate_transactions_raw (
- sale_id int
- , ecrv text
- , sale_date date
- , excluded_from_ratio_study text
- , pin text
- , num_parcels_in_sale int
- , formatted_address text
- , land_sale text
- , community_cd int
- , community_desc text
- , nbhd_cd int
- , nbhd_desc text
- , ward int
- , proptype_cd text
- , proptype_desc text
- , grantee1 text
- , grantee2 text
- , grantor1 text
- , grantor2 text
- , adj_sale_price int
- , gross_sale_price int
- , downpayment int
- , x numeric
- , y numeric
- , fid int
-);
-
-\copy real_estate_transactions_raw from 'zoning/data/raw/real_estate_transactions/Property_Sales_2019_to_2023.csv' with csv header delimiter ',';
-drop table if exists real_estate_transactions;
-
-create table real_estate_transactions (
- id serial primary key
- , parcel_id int references parcel (id)
- , address text
- , sale_date date
- , sale_price numeric
- , neighborhood text
- , property_type text
-);
-
-insert into real_estate_transactions (parcel_id , address , sale_date , sale_price , neighborhood , property_type)
-select
- parcel.id
- , address
- , scraped.sale_date
- , sale_price
- , neighborhood
- , property_type
-from
- real_estate_transactions_scraped as scraped
- join parcel on pid = parcel_id
- and scraped.sale_date <@ valid;
-
diff --git a/etl/segregation.sql b/etl/segregation.sql
deleted file mode 100644
index ba25e911..00000000
--- a/etl/segregation.sql
+++ /dev/null
@@ -1,92 +0,0 @@
-create or replace view categories as select * from (
- values
- ('population_white_non_hispanic'),
- ('population_black_non_hispanic'),
- ('population_hispanic_or_latino'),
- ('population_asian_non_hispanic'),
- ('population_native_hawaiian_or_pacific_islander_non_hispanic'),
- ('population_american_indian_or_alaska_native_non_hispanic'),
- ('population_multiple_races_non_hispanic'),
- ('population_other_non_hispanic')
-) as t (description);
-
-drop type if exists reference_distribution cascade;
-create type reference_distribution as enum (
- 'uniform'
- , 'annual_city'
- , 'average_city'
-);
-
-
--- Segregation index for each tract for each year, computed for each reference
--- distribution.
---
--- The segregation index is the KL-divergence between the distribution of
--- population in a tract and a reference distribution. For example, a tract that
--- has many more white people than the average for the city will have a high
--- segregation index for the 'average_city' distribution.
-
-drop table if exists segregation;
-
-create table segregation as (
-with
- pop_tyc as
- ( -- Population by tract, year, and category
- select id, year_, description, value_
- from acs_tract
- join acs_variable using (name_)
- join categories using (description)
- ),
- pop_ty as
- ( -- Population by tract and year (note: using 'population' variable instead of aggregating categories)
- select id, year_, value_
- from acs_tract join acs_variable using (name_)
- where description = 'population'
- ),
- pop_yc as
- ( -- Population by year and category
- select year_, description, sum(value_) as value_
- from pop_tyc group by year_, description
- ),
- pop_y as
- ( -- Population by year
- select year_, sum(value_) as value_ from pop_ty group by year_
- ),
- dist_yc as
- ( -- Distribution of population by year and category
- select description, c.year_,
- case t.value_ when 0 then 0 else c.value_ / t.value_ end as value_
- from pop_yc as c join pop_y as t using (year_)
- ),
- dist_tyc as
- ( -- Distribution of population by tract, year, and category
- select id, year_, description,
- case t.value_ when 0 then 0 else p.value_ / t.value_ end as value_
- from pop_tyc as p join pop_ty as t using (year_, id)
- ),
- uniform_dist as
- ( -- Uniform distribution across categories
- with n_cat as (select count(*) as n_cat from categories)
- select description, 1.0 / n_cat as value_
- from categories, n_cat
- ),
- average_dist as
- ( -- Average of the annual citywide distributions
- select description, avg(value_) as value_
- from dist_yc
- group by description
- )
-select id, year_, dist, sum(case when p = 0 or q = 0 then 0 else p * ln(p / q) end) as segregation_index
- from
- (
- select id, year_, 'uniform'::reference_distribution as dist, dist_tyc.value_ as p, uniform_dist.value_ as q
- from dist_tyc join uniform_dist using (description)
- union all
- select id, year_, 'annual_city'::reference_distribution as dist, dist_tyc.value_ as p, dist_yc.value_ as q
- from dist_tyc join dist_yc using (year_, description)
- union all
- select id, year_, 'average_city'::reference_distribution as dist, dist_tyc.value_ as p, average_dist.value_ as q
- from dist_tyc join average_dist using (description)
- )
- group by id, year_, dist
-);
diff --git a/etl/usps_migration.sql b/etl/usps_migration.sql
deleted file mode 100644
index 9a123bb4..00000000
--- a/etl/usps_migration.sql
+++ /dev/null
@@ -1,156 +0,0 @@
-drop type if exists usps_migration_flow_direction cascade;
-
-create type usps_migration_flow_direction as enum (
- 'in'
- , 'out'
-);
-
-drop enum if exists usps_migration_flow_type cascade;
-
-create type usps_migration_flow_type as enum (
- 'total'
- , 'business'
- , 'family'
- , 'individual'
- , 'perm'
- , 'temp'
-);
-
-drop table if exists usps_migration cascade;
-
-create table usps_migration (
- date_ date not null check (extract(day from date_) = 1) -- granularity is year-month
- , zip_id int references zip_code (id)
- , direction usps_migration_flow_direction not null
- , type_ usps_migration_flow_type not null
- , flow numeric
- , primary key (date_ , zip_id , direction , type_)
-);
-
-insert into usps_migration with process_date as (
- select
- to_date(yyyymm
- , 'YYYYMM') as date_
- , *
- from
- usps_migration_raw
-)
-, add_zip_id as (
- select
- zip_code.id as zip_id
- , mr.*
- from
- process_date as mr
- join zip_code on zip_code.zip_code = replace(mr.zip_code
- , '='
- , '')
- and zip_code.valid @> to_date(year_::text
- , 'YYYY'))
- select
- date_
- , zip_id
- , 'in'::usps_migration_flow_direction
- , 'total'::usps_migration_flow_type
- , total_from_zip
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'in'::usps_migration_flow_direction
- , 'business'::usps_migration_flow_type
- , total_from_zip_business
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'in'::usps_migration_flow_direction
- , 'family'::usps_migration_flow_type
- , total_from_zip_family
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'in'::usps_migration_flow_direction
- , 'individual'::usps_migration_flow_type
- , total_from_zip_individual
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'in'::usps_migration_flow_direction
- , 'perm'::usps_migration_flow_type
- , total_from_zip_perm
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'in'::usps_migration_flow_direction
- , 'temp'::usps_migration_flow_type
- , total_from_zip_temp
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'out'::usps_migration_flow_direction
- , 'total'::usps_migration_flow_type
- , total_to_zip
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'out'::usps_migration_flow_direction
- , 'business'::usps_migration_flow_type
- , total_to_zip_business
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'out'::usps_migration_flow_direction
- , 'family'::usps_migration_flow_type
- , total_to_zip_family
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'out'::usps_migration_flow_direction
- , 'individual'::usps_migration_flow_type
- , total_to_zip_individual
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'out'::usps_migration_flow_direction
- , 'perm'::usps_migration_flow_type
- , total_to_zip_perm
- from
- add_zip_id
- union all
- select
- date_
- , zip_id
- , 'out'::usps_migration_flow_direction
- , 'temp'::usps_migration_flow_type
- , total_to_zip_temp
- from
- add_zip_id;
-
diff --git a/etl/usps_migration_raw_schema.sql b/etl/usps_migration_raw_schema.sql
deleted file mode 100644
index 50a823ff..00000000
--- a/etl/usps_migration_raw_schema.sql
+++ /dev/null
@@ -1,22 +0,0 @@
-drop table if exists usps_migration_raw cascade;
-
-create table usps_migration_raw (
- yyyymm text
- , zip_code text
- , city text
- , state text
- , total_from_zip numeric
- , total_from_zip_business numeric
- , total_from_zip_family numeric
- , total_from_zip_individual numeric
- , total_from_zip_perm numeric
- , total_from_zip_temp numeric
- , total_to_zip numeric
- , total_to_zip_business numeric
- , total_to_zip_family numeric
- , total_to_zip_individual numeric
- , total_to_zip_perm numeric
- , total_to_zip_temp numeric
- , year_ int
-);
-
diff --git a/etl/zip_schema.sql b/etl/zip_schema.sql
deleted file mode 100644
index 1d9ab6c8..00000000
--- a/etl/zip_schema.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-drop table if exists zip_code cascade;
-
-create table zip_code (
- id serial primary key
- , zip_code text not null
- , valid daterange not null
- , geom geometry(MultiPolygon , 4269) not null
-);
-
-create index zip_code_geom_idx on zip_code using gist (geom);
-
-insert into zip_code (zip_code , valid , geom)
-select
- zcta5ce20
- , '[2020-01-01,)'::daterange
- , geom
-from
- zip_raw_2020
-union
-select
- zcta
- , '[2000-01-01,2020-01-01)'::daterange
- , ST_Transform (geom , 4269)
-from
- zip_raw_2000
From 028368fb246fb2250072a086296f345ee26bb851 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 13:14:58 -0400
Subject: [PATCH 063/142] add dependencies to setup.py
---
setup.py | 52 +++++++++++++++++++++++++++++++---------------------
1 file changed, 31 insertions(+), 21 deletions(-)
diff --git a/setup.py b/setup.py
index 61a0324a..419f142a 100644
--- a/setup.py
+++ b/setup.py
@@ -4,23 +4,27 @@
VERSION = "0.1.0"
TEST_REQUIRES = [
- "pytest",
- "pytest-cov",
- "pytest-xdist",
- "mypy",
- "black",
- "flake8",
- "isort",
- "nbval",
- "nbqa",
- "autoflake",
- ]
+ "pytest",
+ "pytest-cov",
+ "pytest-xdist",
+ "mypy",
+ "black",
+ "flake8",
+ "isort",
+ "nbval",
+ "nbqa",
+ "autoflake",
+]
DEV_REQUIRES = [
"pyro-ppl>=1.8.5",
- "torch", "plotly.express",
- "scipy",
- "chirho", "graphviz",
+ "torch",
+ "plotly.express",
+ "scipy",
+ "chirho",
+ "graphviz",
+ "python-dotenv",
+ "google-cloud-storage",
]
setup(
@@ -31,14 +35,20 @@
author="Basis",
url="https://www.basis.ai/",
project_urls={
- # "Documentation": "",
+ # "Documentation": "",
"Source": "https://github.com/BasisResearch/cities",
},
- install_requires=["jupyter","pandas", "numpy", "scikit-learn","dill", "plotly", "matplotlib>=3.8.2"],
- extras_require={
- "test": TEST_REQUIRES,
- "dev": DEV_REQUIRES + TEST_REQUIRES
- },
+ install_requires=[
+ "jupyter",
+ "pandas",
+ "numpy",
+ "scikit-learn",
+ "dill",
+ "plotly",
+ "matplotlib>=3.8.2",
+ ],
+ extras_require={"test": TEST_REQUIRES, "dev": DEV_REQUIRES + TEST_REQUIRES},
python_requires=">=3.10",
keywords="similarity, causal inference, policymaking, chirho",
- license="Apache 2.0",)
+ license="Apache 2.0",
+)
From 8e7ebf8f0a7b08787f82b89a46864e377a767182 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 13:20:35 -0400
Subject: [PATCH 064/142] format
---
load_data_server/load_server.py | 259 ++++++++++++++++++++------------
1 file changed, 165 insertions(+), 94 deletions(-)
diff --git a/load_data_server/load_server.py b/load_data_server/load_server.py
index cbf82881..602b7295 100644
--- a/load_data_server/load_server.py
+++ b/load_data_server/load_server.py
@@ -14,61 +14,76 @@
load_dotenv()
# Set up logging
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+logging.basicConfig(
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
+)
# DATA INFO
-PROJECT_NAME = os.getenv('GOOGLE_CLOUD_PROJECT')
-BUCKET_NAME = os.getenv('GOOGLE_CLOUD_BUCKET')
+PROJECT_NAME = os.getenv("GOOGLE_CLOUD_PROJECT")
+BUCKET_NAME = os.getenv("GOOGLE_CLOUD_BUCKET")
# Paths inside the bucket
FOLDERS = [
- 'fair_market_rents',
+ "fair_market_rents",
]
# DATABASE INFO
-SCHEMA = os.getenv('SCHEMA')
-HOST = os.getenv('HOST')
-DATABASE = os.getenv('DATABASE')
-USERNAME = os.getenv('USERNAME')
-PASSWORD = os.getenv('PASSWORD')
+SCHEMA = os.getenv("SCHEMA")
+HOST = os.getenv("HOST")
+DATABASE = os.getenv("DATABASE")
+USERNAME = os.getenv("USERNAME")
+PASSWORD = os.getenv("PASSWORD")
OGR2OGR_OPTS = [
- "--config", "PG_USE_COPY", "YES",
+ "--config",
+ "PG_USE_COPY",
+ "YES",
"-progress",
- "-lco", "PRECISION=NO",
+ "-lco",
+ "PRECISION=NO",
"-overwrite",
- "-lco", "GEOMETRY_NAME=geom",
- "-nlt", "PROMOTE_TO_MULTI",
+ "-lco",
+ "GEOMETRY_NAME=geom",
+ "-nlt",
+ "PROMOTE_TO_MULTI",
+]
+DB_OPTS = [
+ f"PG:dbname={DATABASE} host={HOST} user={USERNAME} password={PASSWORD} port=5432"
]
-DB_OPTS = [f"PG:dbname={DATABASE} host={HOST} user={USERNAME} password={PASSWORD} port=5432"]
MAX_RETRIES = 3
RETRY_DELAY = 5 # seconds
+
def get_db_connection():
"""Create a database connection with retries."""
for attempt in range(MAX_RETRIES):
try:
conn = psycopg2.connect(
- host=HOST,
- database=DATABASE,
- user=USERNAME,
- password=PASSWORD
+ host=HOST, database=DATABASE, user=USERNAME, password=PASSWORD
)
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
return conn
except psycopg2.OperationalError as e:
if attempt < MAX_RETRIES - 1:
- logging.warning(f"Connection attempt {attempt + 1} failed. Retrying in {RETRY_DELAY} seconds...")
+ logging.warning(
+ f"Connection attempt {attempt + 1} failed. Retrying in {RETRY_DELAY} seconds..."
+ )
time.sleep(RETRY_DELAY)
else:
- logging.error(f"Failed to connect to the database after {MAX_RETRIES} attempts: {e}")
+ logging.error(
+ f"Failed to connect to the database after {MAX_RETRIES} attempts: {e}"
+ )
raise
+
def create_schema_if_not_exists(conn):
"""Create the schema if it doesn't exist."""
with conn.cursor() as cur:
- cur.execute(f"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = %s);", (SCHEMA,))
+ cur.execute(
+ "SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = %s);",
+ (SCHEMA,),
+ )
schema_exists = cur.fetchone()[0]
if not schema_exists:
@@ -77,68 +92,87 @@ def create_schema_if_not_exists(conn):
else:
logging.info(f"Schema '{SCHEMA}' already exists.")
+
def generate_table_name(blob_name):
"""Generate a PostgreSQL-friendly table name from the blob name, including all parent folders and removing duplicates."""
table_name = os.path.splitext(blob_name)[0]
- path_components = table_name.split('/')
-
+ path_components = table_name.split("/")
+
# Remove any leading empty components
path_components = [comp for comp in path_components if comp]
-
- table_name = '_'.join(path_components)
- table_name = table_name.replace('-', '_').replace('.', '_')
-
- words = table_name.split('_')
+
+ table_name = "_".join(path_components)
+ table_name = table_name.replace("-", "_").replace(".", "_")
+
+ words = table_name.split("_")
unique_words = []
for word in words:
if word.lower() not in (w.lower() for w in unique_words):
unique_words.append(word)
-
- table_name = '_'.join(unique_words)
- table_name = re.sub('_+', '_', table_name)
-
+
+ table_name = "_".join(unique_words)
+ table_name = re.sub("_+", "_", table_name)
+
if table_name[0].isdigit():
- table_name = 'f_' + table_name
-
+ table_name = "f_" + table_name
+
if len(table_name) > 63:
table_name = table_name[:63]
-
- table_name = table_name.rstrip('_')
-
+
+ table_name = table_name.rstrip("_")
+
return table_name.lower()
+
def table_exists(conn, table_name):
"""Check if a table exists in the specified schema."""
with conn.cursor() as cur:
- cur.execute("""
+ cur.execute(
+ """
SELECT EXISTS (
- SELECT FROM information_schema.tables
+ SELECT FROM information_schema.tables
WHERE table_schema = %s AND table_name = %s
);
- """, (SCHEMA, table_name))
+ """,
+ (SCHEMA, table_name),
+ )
return cur.fetchone()[0]
+
def drop_table_if_exists(conn, table_name):
"""Drop the table if it exists."""
with conn.cursor() as cur:
cur.execute(f"DROP TABLE IF EXISTS {SCHEMA}.{table_name} CASCADE;")
+
def load_into_server(conn, file_path, file_type):
table_name = os.path.splitext(os.path.basename(file_path))[0]
full_table_name = f"{SCHEMA}.{table_name}"
-
+
if table_exists(conn, table_name):
drop_table_if_exists(conn, table_name)
-
+
# Upload the file based on its type
- if file_type == 'shp':
- upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-nln", full_table_name] + DB_OPTS + [file_path]
- elif file_type == 'geojson':
- upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-f", "PostgreSQL"] + DB_OPTS + [file_path, "-nln", full_table_name]
+ if file_type == "shp":
+ upload_command = (
+ ["ogr2ogr"]
+ + OGR2OGR_OPTS
+ + ["-nln", full_table_name]
+ + DB_OPTS
+ + [file_path]
+ )
+ elif file_type == "geojson":
+ upload_command = (
+ ["ogr2ogr"]
+ + OGR2OGR_OPTS
+ + ["-f", "PostgreSQL"]
+ + DB_OPTS
+ + [file_path, "-nln", full_table_name]
+ )
else:
logging.error(f"Unsupported file type: {file_type}")
return False
-
+
for attempt in range(MAX_RETRIES):
try:
subprocess.check_call(upload_command)
@@ -146,110 +180,135 @@ def load_into_server(conn, file_path, file_type):
return True
except subprocess.CalledProcessError as e:
if attempt < MAX_RETRIES - 1:
- logging.warning(f"Attempt {attempt + 1} failed for {file_path}. Retrying in {RETRY_DELAY} seconds...")
+ logging.warning(
+ f"Attempt {attempt + 1} failed for {file_path}. Retrying in {RETRY_DELAY} seconds..."
+ )
time.sleep(RETRY_DELAY)
else:
- logging.error(f"Failed to process {file_path} after {MAX_RETRIES} attempts: {e}")
+ logging.error(
+ f"Failed to process {file_path} after {MAX_RETRIES} attempts: {e}"
+ )
return False
+
def group_shapefile_components(blobs):
"""Group Shapefile components together."""
shapefile_groups = {}
for blob in blobs:
name, ext = os.path.splitext(blob.name)
- if ext.lower() in ['.shp', '.shx', '.dbf', '.prj']:
+ if ext.lower() in [".shp", ".shx", ".dbf", ".prj"]:
if name not in shapefile_groups:
shapefile_groups[name] = []
shapefile_groups[name].append(blob)
return shapefile_groups
+
def process_geojson(conn, blob):
table_name = generate_table_name(blob.name)
if table_exists(conn, table_name):
return False # Table already exists, skip processing
-
+
full_table_name = f"{SCHEMA}.{table_name}"
- file_path = os.path.join('/tmp', os.path.basename(blob.name))
+ file_path = os.path.join("/tmp", os.path.basename(blob.name))
blob.download_to_filename(file_path)
-
- upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-f", "PostgreSQL"] + DB_OPTS + [file_path, "-nln", full_table_name]
-
+
+ upload_command = (
+ ["ogr2ogr"]
+ + OGR2OGR_OPTS
+ + ["-f", "PostgreSQL"]
+ + DB_OPTS
+ + [file_path, "-nln", full_table_name]
+ )
+
success = False
for attempt in range(MAX_RETRIES):
try:
- subprocess.check_call(upload_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ subprocess.check_call(
+ upload_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
+ )
success = True
break
except subprocess.CalledProcessError:
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
-
+
os.remove(file_path)
return success
+
def process_shapefile(conn, component_blobs):
- shp_blob = next(blob for blob in component_blobs if blob.name.endswith('.shp'))
+ shp_blob = next(blob for blob in component_blobs if blob.name.endswith(".shp"))
table_name = generate_table_name(shp_blob.name)
-
+
if table_exists(conn, table_name):
return False # Table already exists, skip processing
- temp_dir = os.path.join('/tmp', table_name)
+ temp_dir = os.path.join("/tmp", table_name)
os.makedirs(temp_dir, exist_ok=True)
-
+
for blob in component_blobs:
file_ext = os.path.splitext(blob.name)[1]
file_name = f"{table_name}{file_ext}"
file_path = os.path.join(temp_dir, file_name)
blob.download_to_filename(file_path)
-
+
shp_file = f"{table_name}.shp"
shp_path = os.path.join(temp_dir, shp_file)
-
+
full_table_name = f"{SCHEMA}.{table_name}"
-
- upload_command = ["ogr2ogr"] + OGR2OGR_OPTS + ["-nln", full_table_name] + DB_OPTS + [shp_path]
-
+
+ upload_command = (
+ ["ogr2ogr"] + OGR2OGR_OPTS + ["-nln", full_table_name] + DB_OPTS + [shp_path]
+ )
+
success = False
for attempt in range(MAX_RETRIES):
try:
- subprocess.check_call(upload_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ subprocess.check_call(
+ upload_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
+ )
success = True
break
except subprocess.CalledProcessError:
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
-
+
for file in os.listdir(temp_dir):
os.remove(os.path.join(temp_dir, file))
os.rmdir(temp_dir)
-
+
return success
+
def load_csv_into_server(conn, file_path, full_table_name):
"""Load a CSV file into the PostgreSQL server."""
try:
- with open(file_path, 'r') as f:
+ with open(file_path, "r") as f:
cursor = conn.cursor()
# Read and sanitize the header row
- header = f.readline().strip().split(',')
- sanitized_header = [re.sub(r'[^a-zA-Z0-9_]', '_', col.strip('"').strip()) for col in header]
-
+ header = f.readline().strip().split(",")
+ sanitized_header = [
+ re.sub(r"[^a-zA-Z0-9_]", "_", col.strip('"').strip()) for col in header
+ ]
+
# Ensure column names are unique
seen = set()
- sanitized_header = [col if col not in seen and not seen.add(col) else f"{col}_dup" for col in sanitized_header]
-
+ sanitized_header = [
+ col if col not in seen and not seen.add(col) else f"{col}_dup"
+ for col in sanitized_header
+ ]
+
create_table_sql = f"""
CREATE TABLE {full_table_name} (
{','.join([f'"{col}" TEXT' for col in sanitized_header])}
);
"""
cursor.execute(create_table_sql)
-
+
# Reset file pointer to beginning
f.seek(0)
-
+
# Use COPY to load the data into the table
cursor.copy_expert(f"COPY {full_table_name} FROM STDIN WITH CSV HEADER", f)
conn.commit()
@@ -259,19 +318,20 @@ def load_csv_into_server(conn, file_path, full_table_name):
conn.rollback()
return False
+
def process_csv(conn, blob):
"""Process a CSV file from Google Cloud Storage and load it into the database."""
# Generate a table name based on the blob name
table_name = generate_table_name(blob.name)
full_table_name = f"{SCHEMA}.{table_name}"
-
+
# Check if the table already exists
if table_exists(conn, table_name):
return False # Table already exists, skip processing
-
+
# Download the CSV file to a temporary location
temp_file_name = f"temp_{table_name}.csv"
- temp_file_path = os.path.join('/tmp', temp_file_name)
+ temp_file_path = os.path.join("/tmp", temp_file_name)
blob.download_to_filename(temp_file_path)
try:
@@ -283,45 +343,48 @@ def process_csv(conn, blob):
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
+
def count_processable_files(blobs):
"""Count the number of files that will be processed."""
count = 0
shapefile_groups = group_shapefile_components(blobs)
for blob in blobs:
- if blob.name.endswith('.geojson') or blob.name.endswith('.csv'):
+ if blob.name.endswith(".geojson") or blob.name.endswith(".csv"):
count += 1
- elif blob.name.endswith('.shp'):
+ elif blob.name.endswith(".shp"):
base_name = os.path.splitext(blob.name)[0]
if base_name in shapefile_groups:
count += 1
return count
+
def process_file(conn, blob, shapefile_groups, processed_shapefiles):
"""Process a single file and return whether it was processed."""
- if blob.name.endswith('.geojson'):
+ if blob.name.endswith(".geojson"):
return process_geojson(conn, blob)
- elif blob.name.endswith('.shp'):
+ elif blob.name.endswith(".shp"):
base_name = os.path.splitext(blob.name)[0]
if base_name in shapefile_groups and base_name not in processed_shapefiles:
success = process_shapefile(conn, shapefile_groups[base_name])
if success:
processed_shapefiles.add(base_name)
return success
- elif blob.name.endswith('.csv'):
+ elif blob.name.endswith(".csv"):
return process_csv(conn, blob)
return False
-def download_and_process_files(bucket, conn, folder_prefix=''):
+
+def download_and_process_files(bucket, conn, folder_prefix=""):
"""Download and process files from the specified folder and its subfolders in the GCS bucket."""
blobs = list(bucket.list_blobs(prefix=folder_prefix))
total_files = count_processable_files(blobs)
shapefile_groups = group_shapefile_components(blobs)
-
+
processed_shapefiles = set()
-
+
with tqdm(total=total_files, desc="Processing files", unit="file") as pbar:
for blob in blobs:
- if blob.name.endswith('/'): # This is a folder
+ if blob.name.endswith("/"): # This is a folder
continue
processed = process_file(conn, blob, shapefile_groups, processed_shapefiles)
if processed:
@@ -330,12 +393,13 @@ def download_and_process_files(bucket, conn, folder_prefix=''):
pbar.total -= 1
pbar.refresh()
+
def main(process_entire_bucket=False):
try:
# Initialize Google Cloud Storage client
storage_client = storage.Client(project=PROJECT_NAME)
bucket = storage_client.bucket(BUCKET_NAME)
-
+
# Connect to the database
conn = get_db_connection()
create_schema_if_not_exists(conn)
@@ -354,12 +418,19 @@ def main(process_entire_bucket=False):
except Exception as e:
print(f"An error occurred: {e}")
finally:
- if 'conn' in locals() and conn:
+ if "conn" in locals() and conn:
conn.close()
+
if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Process files from Google Cloud Storage bucket")
- parser.add_argument('--full-bucket', action='store_true', help='Process the entire bucket instead of specific folders')
+ parser = argparse.ArgumentParser(
+ description="Process files from Google Cloud Storage bucket"
+ )
+ parser.add_argument(
+ "--full-bucket",
+ action="store_true",
+ help="Process the entire bucket instead of specific folders",
+ )
args = parser.parse_args()
- main(process_entire_bucket=args.full_bucket)
\ No newline at end of file
+ main(process_entire_bucket=args.full_bucket)
From d2cfa81035b1778b8fcf4cb0e68ecd8fb5fa21c4 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 13:25:08 -0400
Subject: [PATCH 065/142] ensure postgis is loaded
---
load_data_server/load_server.py | 13 ++-----------
1 file changed, 2 insertions(+), 11 deletions(-)
diff --git a/load_data_server/load_server.py b/load_data_server/load_server.py
index 602b7295..f0466aad 100644
--- a/load_data_server/load_server.py
+++ b/load_data_server/load_server.py
@@ -80,17 +80,8 @@ def get_db_connection():
def create_schema_if_not_exists(conn):
"""Create the schema if it doesn't exist."""
with conn.cursor() as cur:
- cur.execute(
- "SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = %s);",
- (SCHEMA,),
- )
- schema_exists = cur.fetchone()[0]
-
- if not schema_exists:
- cur.execute(f"CREATE SCHEMA {SCHEMA};")
- logging.info(f"Schema '{SCHEMA}' created.")
- else:
- logging.info(f"Schema '{SCHEMA}' already exists.")
+ cur.execute(f"create schema if not exists {SCHEMA};")
+ cur.execute("create extension if not exists postgis;")
def generate_table_name(blob_name):
From 6870b936e51f0dc23b4eecc0776882d86d830e9f Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 13:28:26 -0400
Subject: [PATCH 066/142] unconditionally load all data
---
load_data_server/load_server.py | 29 ++---------------------------
1 file changed, 2 insertions(+), 27 deletions(-)
diff --git a/load_data_server/load_server.py b/load_data_server/load_server.py
index f0466aad..4dd98015 100644
--- a/load_data_server/load_server.py
+++ b/load_data_server/load_server.py
@@ -115,21 +115,6 @@ def generate_table_name(blob_name):
return table_name.lower()
-def table_exists(conn, table_name):
- """Check if a table exists in the specified schema."""
- with conn.cursor() as cur:
- cur.execute(
- """
- SELECT EXISTS (
- SELECT FROM information_schema.tables
- WHERE table_schema = %s AND table_name = %s
- );
- """,
- (SCHEMA, table_name),
- )
- return cur.fetchone()[0]
-
-
def drop_table_if_exists(conn, table_name):
"""Drop the table if it exists."""
with conn.cursor() as cur:
@@ -140,8 +125,7 @@ def load_into_server(conn, file_path, file_type):
table_name = os.path.splitext(os.path.basename(file_path))[0]
full_table_name = f"{SCHEMA}.{table_name}"
- if table_exists(conn, table_name):
- drop_table_if_exists(conn, table_name)
+ drop_table_if_exists(conn, table_name)
# Upload the file based on its type
if file_type == "shp":
@@ -196,9 +180,6 @@ def group_shapefile_components(blobs):
def process_geojson(conn, blob):
table_name = generate_table_name(blob.name)
- if table_exists(conn, table_name):
- return False # Table already exists, skip processing
-
full_table_name = f"{SCHEMA}.{table_name}"
file_path = os.path.join("/tmp", os.path.basename(blob.name))
@@ -232,9 +213,6 @@ def process_shapefile(conn, component_blobs):
shp_blob = next(blob for blob in component_blobs if blob.name.endswith(".shp"))
table_name = generate_table_name(shp_blob.name)
- if table_exists(conn, table_name):
- return False # Table already exists, skip processing
-
temp_dir = os.path.join("/tmp", table_name)
os.makedirs(temp_dir, exist_ok=True)
@@ -291,6 +269,7 @@ def load_csv_into_server(conn, file_path, full_table_name):
]
create_table_sql = f"""
+ drop table if exists {full_table_name};
CREATE TABLE {full_table_name} (
{','.join([f'"{col}" TEXT' for col in sanitized_header])}
);
@@ -316,10 +295,6 @@ def process_csv(conn, blob):
table_name = generate_table_name(blob.name)
full_table_name = f"{SCHEMA}.{table_name}"
- # Check if the table already exists
- if table_exists(conn, table_name):
- return False # Table already exists, skip processing
-
# Download the CSV file to a temporary location
temp_file_name = f"temp_{table_name}.csv"
temp_file_path = os.path.join("/tmp", temp_file_name)
From a72f6f0e02d61fcf4bc5e600cd3cd2d07e246306 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 14:06:52 -0400
Subject: [PATCH 067/142] null out missing data in residential_permits
---
dbt/models/residential_permits.sql | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index c4fb4267..35018922 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -22,9 +22,9 @@ select
, memory_car as num_memory_care_units
, assisted as num_assisted_living_units
, com_off_re = 'Y' as is_commercial_and_residential
- , sqf as square_feet
+ , nullif(sqf, 0) as square_feet
, public_fun = 'Y' as is_public_funded
- , permit_val as permit_value
+ , nullif(permit_val, 0) as permit_value
, community_ as community_designation
, notes
, st_transform(geom, {{ var("srid") }}) as geom
From d33588ebad1aad27fb8d9073e47a4e6469b260fc Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 15:33:05 -0400
Subject: [PATCH 068/142] ensure table drop succeeds
---
load_data_server/load_server.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/load_data_server/load_server.py b/load_data_server/load_server.py
index 4dd98015..8c3089e8 100644
--- a/load_data_server/load_server.py
+++ b/load_data_server/load_server.py
@@ -269,7 +269,7 @@ def load_csv_into_server(conn, file_path, full_table_name):
]
create_table_sql = f"""
- drop table if exists {full_table_name};
+ drop table if exists {full_table_name} cascade;
CREATE TABLE {full_table_name} (
{','.join([f'"{col}" TEXT' for col in sanitized_header])}
);
From 6c3b5757e14249abf599a6733929d6f0f8381a01 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 15:57:31 -0400
Subject: [PATCH 069/142] start adding tables to be used by the model
---
dbt/models/housing_units_by_census_tracts.sql | 62 +++++++++++++++++++
.../property_values_by_census_tracts.sql | 22 +++++++
.../residential_permits_to_census_tracts.sql | 31 ++++++++++
3 files changed, 115 insertions(+)
create mode 100644 dbt/models/housing_units_by_census_tracts.sql
create mode 100644 dbt/models/property_values_by_census_tracts.sql
create mode 100644 dbt/models/residential_permits_to_census_tracts.sql
diff --git a/dbt/models/housing_units_by_census_tracts.sql b/dbt/models/housing_units_by_census_tracts.sql
new file mode 100644
index 00000000..f73b49f5
--- /dev/null
+++ b/dbt/models/housing_units_by_census_tracts.sql
@@ -0,0 +1,62 @@
+with census_tracts as (
+ select
+ census_tract_id
+ , statefp || countyfp || tractce as census_tract
+ from {{ ref('census_tracts') }}
+)
+, parcels as (
+ select
+ parcel_id
+ , geom
+ from {{ ref('parcels') }}
+)
+, residential_permits as (
+ select
+ residential_permit_id
+ , year_
+ , permit_value
+ from {{ ref('residential_permits') }}
+)
+, residential_permits_to_parcels as (
+ select
+ residential_permit_id
+ , parcel_id
+ from {{ ref('residential_permits_to_parcels') }}
+)
+, residential_permits_to_census_tracts as (
+ select
+ residential_permit_id
+ , census_tract_id
+ from {{ ref('residential_permits_to_census_tracts') }}
+)
+, residential as (
+ select
+ census_tracts.census_tract
+ , residential_permits.year_
+ , residential_permits.housing_units
+ , st_area(parcels.geom) as parcel_sqm
+ , residential_permits.permit_value
+ from
+ residential_permits
+ inner join residential_permits_to_parcels using (residential_permit_id)
+ inner join parcels using (parcel_id)
+ inner join residential_permits_to_census_tracts using (residential_permit_id)
+ inner join census_tracts using (census_tract_id)
+ where year_ <= 2020
+)
+, agg_residential as (
+ select
+ census_tract
+ , year_
+ , sum(housing_units) as housing_units
+ from residential
+ group by census_tract, year_
+)
+
+select
+ census_tract
+ , year_
+ , housing_units -- do we really want the total _applied_ units, or should we
+ -- be looking at the total unit estimates from ACS?
+from
+ agg_residential
diff --git a/dbt/models/property_values_by_census_tracts.sql b/dbt/models/property_values_by_census_tracts.sql
new file mode 100644
index 00000000..0c187448
--- /dev/null
+++ b/dbt/models/property_values_by_census_tracts.sql
@@ -0,0 +1,22 @@
+-- Median and total parcel property values aggregated by census tract.
+
+with parcels as (
+ select
+ parcel_id
+ , emv_total
+ from {{ ref('parcels_base') }}
+)
+, parcels_to_census_tracts as (
+ select
+ parcel_id
+ , census_tract_id
+ from {{ ref('parcels_to_census_tracts') }}
+)
+select
+ parcels_to_census_tracts.census_tract_id
+ , sum(parcels.emv_total) as total_value
+ , percentile_cont(0.5) within group (order by parcels.emv_total) as median_value
+from
+ parcels_to_census_tracts using (parcel_id)
+ inner join parcels using (parcel_id)
+group by census_tracts.census_tract_id
diff --git a/dbt/models/residential_permits_to_census_tracts.sql b/dbt/models/residential_permits_to_census_tracts.sql
new file mode 100644
index 00000000..79a48be4
--- /dev/null
+++ b/dbt/models/residential_permits_to_census_tracts.sql
@@ -0,0 +1,31 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['residential_permit_id']},
+ {'columns': ['census_tract_id']}
+ ]
+ )
+}}
+
+with
+residential_permits as (
+ select
+ residential_permit_id as id
+ , daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
+ , geom
+ from {{ ref("residential_permits") }}
+)
+, census_tracts as (
+ select
+ census_tract_id as id
+ , valid
+ , geom
+ from {{ ref("census_tracts") }}
+)
+select
+ child_id as residential_permit_id
+ , parent_id as census_tract_id
+ , valid
+ , type_
+from {{ tag_regions("residential_permits", "census_tracts") }}
From a820807868005e02adf7a501c67fd7eb7a619058 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 20 Aug 2024 16:59:18 -0400
Subject: [PATCH 070/142] fix misnamed field
---
dbt/models/city_boundary.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dbt/models/city_boundary.sql b/dbt/models/city_boundary.sql
index 88af8782..d9bfa060 100644
--- a/dbt/models/city_boundary.sql
+++ b/dbt/models/city_boundary.sql
@@ -1,5 +1,5 @@
select
- ogc_id as city_boundary_id
+ ogc_fid as city_boundary_id
, st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'city_boundary_minneapolis') }}
From 9590725f0d9ec89f56e3525d9f4e2cb82dc37728 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 11:43:15 -0400
Subject: [PATCH 071/142] remove last references to old raw schema
---
dbt/models/acs_block_group.sql | 2 +-
dbt/models/acs_block_group_clean.sql | 22 ++++++++++++++++++++++
dbt/models/acs_tract_clean.sql | 14 +++++++-------
dbt/models/schema.yml | 8 ++------
4 files changed, 32 insertions(+), 14 deletions(-)
create mode 100644 dbt/models/acs_block_group_clean.sql
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index 98545ebb..6165c8ac 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -29,7 +29,7 @@ census_block_groups as (
, name_
, value_
from
- {{ source('minneapolis_old', 'acs_bg_raw') }}
+ {{ ref('acs_block_group_clean') }}
)
select
census_block_groups.census_block_group_id
diff --git a/dbt/models/acs_block_group_clean.sql b/dbt/models/acs_block_group_clean.sql
new file mode 100644
index 00000000..d629d782
--- /dev/null
+++ b/dbt/models/acs_block_group_clean.sql
@@ -0,0 +1,22 @@
+with
+acs_bg_raw as (
+ select
+ statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , year
+ , code
+ , value
+ from {{ source('minneapolis', 'acs_bg_raw') }}
+)
+select
+ statefp
+ , countyfp
+ , tractce
+ , blkgrpce
+ , year as year_
+ , code as name_
+ , case when "value" < 0 then null else "value" end as value_
+from
+ acs_bg_raw
diff --git a/dbt/models/acs_tract_clean.sql b/dbt/models/acs_tract_clean.sql
index bd5638a8..1c631ff4 100644
--- a/dbt/models/acs_tract_clean.sql
+++ b/dbt/models/acs_tract_clean.sql
@@ -4,17 +4,17 @@ acs_tract_raw as (
statefp
, countyfp
, tractce
- , year_
- , name_
- , value_
- from {{ source('minneapolis_old', 'acs_tract_raw') }}
+ , year
+ , code
+ , value
+ from {{ source('minneapolis', 'acs_tract_raw') }}
)
select
statefp
, countyfp
, tractce
- , year_
- , name_
- , case when value_ < 0 then null else value_ end as value_
+ , year as year_
+ , code as name_
+ , case when "value" < 0 then null else "value" end as value_
from
acs_tract_raw
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index f78e1251..f9b0ddab 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -1,14 +1,10 @@
sources:
- - name: minneapolis_old
- database: cities
- schema: public
- tables:
- - name: acs_bg_raw
- - name: acs_tract_raw
- name: minneapolis
database: cities
schema: minneapolis
tables:
+ - name: acs_bg_raw
+ - name: acs_tract_raw
- name: residential_permits_residentialpermits
- name: commercial_permits_nonresidentialconstruction
- name: high_frequency_transit_2015_freq_350_ft_buffer
From 42847e5e8382548386839fef3f894c485e9559ab Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 11:43:29 -0400
Subject: [PATCH 072/142] add new loader for acs data
---
load_data_server/load_acs.py | 183 +++++++++++++++++++++++++++++++++++
1 file changed, 183 insertions(+)
create mode 100644 load_data_server/load_acs.py
diff --git a/load_data_server/load_acs.py b/load_data_server/load_acs.py
new file mode 100644
index 00000000..23ae704e
--- /dev/null
+++ b/load_data_server/load_acs.py
@@ -0,0 +1,183 @@
+import logging
+import os
+import tempfile
+
+from dotenv import load_dotenv
+from google.cloud import storage
+import psycopg2
+from tqdm import tqdm
+
+# Load environment variables
+load_dotenv()
+
+# Set up logging
+logging.basicConfig(
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
+)
+
+# DATA INFO
+PROJECT_NAME = os.getenv("GOOGLE_CLOUD_PROJECT")
+BUCKET_NAME = os.getenv("GOOGLE_CLOUD_BUCKET")
+
+# DATABASE INFO
+SCHEMA = os.getenv("SCHEMA")
+HOST = os.getenv("HOST")
+DATABASE = os.getenv("DATABASE")
+USERNAME = os.getenv("USERNAME")
+PASSWORD = os.getenv("PASSWORD")
+
+YEAR_RANGE = range(2013, 2023)
+ACS_CODES = {
+ "B03002_003E": "population_white_non_hispanic",
+ "B03002_004E": "population_black_non_hispanic",
+ "B03002_005E": "population_asian_non_hispanic",
+ "B03002_006E": "population_native_hawaiian_or_pacific_islander_non_hispanic",
+ "B03002_007E": "population_american_indian_or_alaska_native_non_hispanic",
+ "B03002_008E": "population_other_non_hispanic",
+ "B03002_009E": "population_multiple_races_non_hispanic",
+ "B03002_010E": "population_multiple_races_and_other_non_hispanic",
+ "B07204_001E": "geographic_mobility_total_responses",
+ "B07204_002E": "geographic_mobility_same_house_1_year_ago",
+ "B07204_004E": "geographic_mobility_different_house_1_year_ago_same_city",
+ "B07204_005E": "geographic_mobility_different_house_1_year_ago_same_county",
+ "B07204_006E": "geographic_mobility_different_house_1_year_ago_same_state",
+ "B07204_007E": "geographic_mobility_different_house_1_year_ago_same_country",
+ "B07204_016E": "geographic_mobility_different_house_1_year_ago_abroad",
+ "B01003_001E": "population",
+ "B02001_002E": "white",
+ "B02001_003E": "black",
+ "B02001_004E": "american_indian_or_alaska_native",
+ "B02001_005E": "asian",
+ "B02001_006E": "native_hawaiian_or_pacific_islander",
+ "B03001_003E": "population_hispanic_or_latino",
+ "B02001_007E": "other_race",
+ "B02001_008E": "multiple_races",
+ "B02001_009E": "multiple_races_and_other_race",
+ "B02001_010E": "two_or_more_races_excluding_other",
+ "B02015_002E": "east_asian_chinese",
+ "B02015_003E": "east_asian_hmong",
+ "B02015_004E": "east_asian_japanese",
+ "B02015_005E": "east_asian_korean",
+ "B02015_006E": "east_asian_mongolian",
+ "B02015_007E": "east_asian_okinawan",
+ "B02015_008E": "east_asian_taiwanese",
+ "B02015_009E": "east_asian_other",
+ "B02015_010E": "southeast_asian_burmese",
+ "B02015_011E": "southeast_asian_cambodian",
+ "B02015_012E": "southeast_asian_filipino",
+ "B02015_013E": "southeast_asian_indonesian",
+ "B02015_014E": "southeast_asian_laotian",
+ "B02015_015E": "southeast_asian_malaysian",
+ "B02015_016E": "southeast_asian_mien",
+ "B02015_017E": "southeast_asian_singaporean",
+ "B02015_018E": "southeast_asian_thai",
+ "B02015_019E": "southeast_asian_viet",
+ "B02015_020E": "southeast_asian_other",
+ "B02015_021E": "south_asian_asian_indian",
+ "B02015_022E": "south_asian_bangladeshi",
+ "B02015_023E": "south_asian_bhutanese",
+ "B02015_024E": "south_asian_nepalese",
+ "B02015_025E": "south_asian_pakistani",
+ "B02015_026E": "south_asian_sikh",
+ "B02015_027E": "south_asian_sri_lankan",
+ "B02015_028E": "south_asian_other",
+ "B02015_029E": "central_asian_kazakh",
+ "B02015_030E": "central_asian_uzbek",
+ "B02015_031E": "central_asian_other",
+ "B02015_032E": "other_asian_specified",
+ "B02015_033E": "other_asian_not_specified",
+ "B19013_001E": "median_household_income",
+ "B19013A_001E": "median_household_income_white",
+ "B19013H_001E": "median_household_income_white_non_hispanic",
+ "B19013I_001E": "median_household_income_hispanic",
+ "B19013B_001E": "median_household_income_black",
+ "B19013C_001E": "median_household_income_american_indian_or_alaska_native",
+ "B19013D_001E": "median_household_income_asian",
+ "B19013E_001E": "median_household_income_native_hawaiian_or_pacific_islander",
+ "B19013F_001E": "median_household_income_other_race",
+ "B19013G_001E": "median_household_income_multiple_races",
+ "B19019_002E": "median_household_income_1_person_households",
+ "B19019_003E": "median_household_income_2_person_households",
+ "B19019_004E": "median_household_income_3_person_households",
+ "B19019_005E": "median_household_income_4_person_households",
+ "B19019_006E": "median_household_income_5_person_households",
+ "B19019_007E": "median_household_income_6_person_households",
+ "B19019_008E": "median_household_income_7_or_more_person_households",
+ "B01002_001E": "median_age",
+ "B01002_002E": "median_age_male",
+ "B01002_003E": "median_age_female",
+ "B25031_001E": "median_gross_rent",
+ "B25031_002E": "median_gross_rent_0_bedrooms",
+ "B25031_003E": "median_gross_rent_1_bedrooms",
+ "B25031_004E": "median_gross_rent_2_bedrooms",
+ "B25031_005E": "median_gross_rent_3_bedrooms",
+ "B25031_006E": "median_gross_rent_4_bedrooms",
+ "B25031_007E": "median_gross_rent_5_bedrooms",
+ "B25032_001E": "total_housing_units",
+ "B25032_002E": "total_owner_occupied_housing_units",
+ "B25032_013E": "total_renter_occupied_housing_units",
+ "B25070_001E": "median_gross_rent_as_percentage_of_household_income",
+}
+
+
+if __name__ == "__main__":
+ conn = psycopg2.connect(
+ host=HOST, database=DATABASE, user=USERNAME, password=PASSWORD
+ )
+ storage_client = storage.Client(project=PROJECT_NAME)
+ bucket = storage_client.bucket(BUCKET_NAME)
+ cur = conn.cursor()
+
+ cur.execute(f"drop table if exists {SCHEMA}.acs_tract_raw")
+ cur.execute(
+ f"create table {SCHEMA}.acs_tract_raw (statefp text, countyfp text, tractce text, year int, code text, value numeric)"
+ )
+
+ temp_table = f"{SCHEMA}.acs_tract_temp"
+ cur.execute(f"drop table if exists {temp_table}")
+ cur.execute(
+ f"create table {temp_table} (statefp text, countyfp text, tractce text, value numeric)"
+ )
+ for code in tqdm(ACS_CODES.keys()):
+ desc = ACS_CODES[code]
+
+ for blob in bucket.list_blobs(prefix=f"acs/tracts/{desc}/"):
+ year = blob.name.split("/")[-1].split(".")[0]
+ cur.execute(f"truncate {temp_table}")
+ with tempfile.NamedTemporaryFile() as temp:
+ blob.download_to_filename(temp.name)
+ cur.copy_expert(f"copy {temp_table} from stdin with csv header", temp)
+
+ cur.execute(
+ f"insert into {SCHEMA}.acs_tract_raw select statefp, countyfp, tractce, %s, %s, value from {temp_table}",
+ (year, code),
+ )
+ cur.execute(f"drop table {temp_table}")
+ conn.commit()
+
+ cur.execute(f"drop table if exists {SCHEMA}.acs_bg_raw")
+ cur.execute(
+ f"create table {SCHEMA}.acs_bg_raw (statefp text, countyfp text, tractce text, blkgrpce text, year int, code text, value numeric)"
+ )
+
+ temp_table = f"{SCHEMA}.acs_tract_temp"
+ cur.execute(f"drop table if exists {temp_table}")
+ cur.execute(
+ f"create table {temp_table} (statefp text, countyfp text, tractce text, blkgrpce text, value numeric)"
+ )
+
+ for code in tqdm(ACS_CODES.keys()):
+ desc = ACS_CODES[code]
+ for blob in bucket.list_blobs(prefix=f"acs/block_groups/{desc}/"):
+ year = blob.name.split("/")[-1].split(".")[0]
+ cur.execute(f"truncate {temp_table}")
+ with tempfile.NamedTemporaryFile() as temp:
+ blob.download_to_filename(temp.name)
+ cur.copy_expert(f"copy {temp_table} from stdin with csv header", temp)
+
+ cur.execute(
+ f"insert into {SCHEMA}.acs_bg_raw select statefp, countyfp, tractce, blkgrpce, %s, %s value from {temp_table}",
+ (year, code),
+ )
+ cur.execute(f"drop table {temp_table}")
+ conn.commit()
From 686ee09639cfd733a4b2785c08aec877eafd3bdd Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 11:43:48 -0400
Subject: [PATCH 073/142] correctly refer to seed
---
dbt/models/segregation_indexes.sql | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/dbt/models/segregation_indexes.sql b/dbt/models/segregation_indexes.sql
index 2d2c3cac..8d50587f 100644
--- a/dbt/models/segregation_indexes.sql
+++ b/dbt/models/segregation_indexes.sql
@@ -17,18 +17,24 @@ with
, value_
from {{ ref("acs_tract") }}
)
+ , acs_variables as (
+ select
+ "variable" as name_
+ , description
+ from {{ ref("acs_variables") }}
+ )
, pop_tyc as
( -- Population by tract, year, and category
select acs_tract.census_tract_id, acs_tract.year_, categories.category, acs_tract.value_
from acs_tract
- join acs_variable using (name_)
- join categories on categories.category = acs_variable.description
+ join acs_variables using (name_)
+ join categories on categories.category = acs_variables.description
),
pop_ty as
( -- Population by tract and year (note: using 'population' variable instead of aggregating categories)
select census_tract_id, year_, value_
- from acs_tract join acs_variable using (name_)
- where acs_variable.description = 'population'
+ from acs_tract join acs_variables using (name_)
+ where acs_variables.description = 'population'
),
pop_yc as
( -- Population by year and category
From 30323c4a3a92c3366cc64a493c326132862fa374 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 11:49:08 -0400
Subject: [PATCH 074/142] more work on tables for model
---
dbt/models/high_frequency_transit_lines.sql | 26 ++++++++++++-------
.../high_frequency_transit_lines_union.sql | 4 +--
dbt/models/high_frequency_transit_stops.sql | 16 ++----------
dbt/models/housing_units_by_census_tracts.sql | 9 ++++---
dbt/models/parcels_distance_to_transit.sql | 21 +++++++++++++++
dbt/models/parcels_to_census_tracts.sql | 25 ++++++++++++++++++
.../property_values_by_census_tracts.sql | 4 +--
7 files changed, 74 insertions(+), 31 deletions(-)
create mode 100644 dbt/models/parcels_distance_to_transit.sql
create mode 100644 dbt/models/parcels_to_census_tracts.sql
diff --git a/dbt/models/high_frequency_transit_lines.sql b/dbt/models/high_frequency_transit_lines.sql
index af4c344c..34cd9779 100644
--- a/dbt/models/high_frequency_transit_lines.sql
+++ b/dbt/models/high_frequency_transit_lines.sql
@@ -1,22 +1,30 @@
with lines as (
select
- year_
+ valid
, geom
from {{ ref('high_frequency_transit_lines_union') }}
)
, stops as (
select
- year_
+ valid
, geom
from {{ ref('high_frequency_transit_stops') }}
)
+, lines_and_stops as (
+ select
+ lines.valid * stops.valid as valid
+ , lines.geom as line_geom
+ , stops.geom as stop_geom
+ from
+ lines
+ inner join stops on lines.valid && stops.valid
+)
select
- year_ as high_frequency_transit_lines_id
- , year_
- , lines.geom
+ {{ dbt_utils.generate_surrogate_key(['valid']) }} as high_frequency_transit_line_id
+ , valid
+ , line_geom as geom
-- note units are in meters
- , st_buffer(lines.geom, 106.7) as blue_zone_geom -- 350 feet
- , st_union(st_buffer(lines.geom, 402.3), st_buffer(stops.geom, 804.7)) as yellow_zone_geom -- quarter mile around lines and half mile around stops
+ , st_buffer(line_geom, 106.7) as blue_zone_geom -- 350 feet
+ , st_union(st_buffer(line_geom, 402.3), st_buffer(stop_geom, 804.7)) as yellow_zone_geom -- quarter mile around lines and half mile around stops
from
- lines
- inner join stops using (year_)
+ lines_and_stops
diff --git a/dbt/models/high_frequency_transit_lines_union.sql b/dbt/models/high_frequency_transit_lines_union.sql
index 073ec9a1..8b361587 100644
--- a/dbt/models/high_frequency_transit_lines_union.sql
+++ b/dbt/models/high_frequency_transit_lines_union.sql
@@ -11,11 +11,11 @@ with lines_2015 as (
{{ source('minneapolis', 'high_frequency_transit_2016_freq_lines') }}
)
select
- 2015 as year_,
+ '(,2016-01-01)'::daterange as valid,
geom
from lines_2015
union all
select
- 2016 as year_,
+ '[2016-01-01,)'::daterange as valid,
geom
from lines_2016
diff --git a/dbt/models/high_frequency_transit_stops.sql b/dbt/models/high_frequency_transit_stops.sql
index b751153f..9d9a0459 100644
--- a/dbt/models/high_frequency_transit_stops.sql
+++ b/dbt/models/high_frequency_transit_stops.sql
@@ -1,21 +1,9 @@
with stops_2015 as (
select
- 2015 as year_
- , st_union(st_transform(geom, {{ var("srid") }}))::geometry(multipoint, {{ var("srid") }}) as geom
+ st_union(st_transform(geom, {{ var("srid") }}))::geometry(multipoint, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'high_frequency_transit_2015_freq_rail_stops') }}
)
-, stops_2016 as ( -- stops are unchanged in 2016
- select
- 2016 as year_
- , geom
- from stops_2015
-)
select
- year_
+ '[,]'::daterange as valid
, geom
from stops_2015
-union all
-select
- year_
- , geom
-from stops_2016
diff --git a/dbt/models/housing_units_by_census_tracts.sql b/dbt/models/housing_units_by_census_tracts.sql
index f73b49f5..208c12f3 100644
--- a/dbt/models/housing_units_by_census_tracts.sql
+++ b/dbt/models/housing_units_by_census_tracts.sql
@@ -15,6 +15,7 @@ with census_tracts as (
residential_permit_id
, year_
, permit_value
+ , num_units
from {{ ref('residential_permits') }}
)
, residential_permits_to_parcels as (
@@ -33,7 +34,7 @@ with census_tracts as (
select
census_tracts.census_tract
, residential_permits.year_
- , residential_permits.housing_units
+ , residential_permits.num_units
, st_area(parcels.geom) as parcel_sqm
, residential_permits.permit_value
from
@@ -48,7 +49,7 @@ with census_tracts as (
select
census_tract
, year_
- , sum(housing_units) as housing_units
+ , sum(num_units) as num_units
from residential
group by census_tract, year_
)
@@ -56,7 +57,7 @@ with census_tracts as (
select
census_tract
, year_
- , housing_units -- do we really want the total _applied_ units, or should we
- -- be looking at the total unit estimates from ACS?
+ , num_units -- do we really want the total _applied_ units, or should we be
+ -- looking at the total unit estimates from ACS?
from
agg_residential
diff --git a/dbt/models/parcels_distance_to_transit.sql b/dbt/models/parcels_distance_to_transit.sql
new file mode 100644
index 00000000..c7881209
--- /dev/null
+++ b/dbt/models/parcels_distance_to_transit.sql
@@ -0,0 +1,21 @@
+with
+ parcels as (
+ select
+ parcel_id
+ , valid
+ , geom
+ from {{ ref('parcels_base') }}
+ )
+ , high_frequency_transit_lines as (
+ select
+ valid
+ , geom
+ from {{ ref('high_frequency_transit_lines') }}
+ )
+select
+ parcels.parcel_id
+ , st_distance(parcels.geom, high_frequency_transit_lines.geom) as distance
+from
+ parcels
+ inner join high_frequency_transit_lines
+ on parcels.valid && high_frequency_transit_lines.valid
diff --git a/dbt/models/parcels_to_census_tracts.sql b/dbt/models/parcels_to_census_tracts.sql
new file mode 100644
index 00000000..d3c2d6e0
--- /dev/null
+++ b/dbt/models/parcels_to_census_tracts.sql
@@ -0,0 +1,25 @@
+with
+parcels as (
+ select
+ parcel_id
+ from {{ ref("parcels_base") }}
+)
+, census_block_groups as (
+ select
+ census_block_group_id
+ , census_tract_id
+ from {{ ref("census_block_groups") }}
+)
+, parcels_to_census_block_groups as (
+ select
+ parcel_id
+ , census_block_group_id
+ from {{ ref("parcels_to_census_block_groups") }}
+)
+select
+ parcels.parcel_id
+ , census_block_groups.census_tract_id
+from
+ parcels
+ left join parcels_to_census_block_groups using (parcel_id)
+ left join census_block_groups using (census_block_group_id)
diff --git a/dbt/models/property_values_by_census_tracts.sql b/dbt/models/property_values_by_census_tracts.sql
index 0c187448..51efa233 100644
--- a/dbt/models/property_values_by_census_tracts.sql
+++ b/dbt/models/property_values_by_census_tracts.sql
@@ -17,6 +17,6 @@ select
, sum(parcels.emv_total) as total_value
, percentile_cont(0.5) within group (order by parcels.emv_total) as median_value
from
- parcels_to_census_tracts using (parcel_id)
+ parcels_to_census_tracts
inner join parcels using (parcel_id)
-group by census_tracts.census_tract_id
+group by parcels_to_census_tracts.census_tract_id
From 5800b5dffb20f91e6f28574c08fdeb310c361295 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 14:57:21 -0400
Subject: [PATCH 075/142] more work on tables for model
---
dbt/macros/median.sql | 3 +
dbt/macros/standardize.sql | 6 ++
dbt/models/census_block_groups.sql | 2 +
dbt/models/census_tracts.sql | 14 +++-
.../census_tracts_distance_to_transit.sql | 36 ++++++++++
dbt/models/census_tracts_housing_units.sql | 36 ++++++++++
dbt/models/census_tracts_parcel_area.sql | 34 ++++++++++
dbt/models/census_tracts_property_values.sql | 37 +++++++++++
dbt/models/census_tracts_wide.sql | 66 +++++++++++++++++++
dbt/models/high_frequency_transit_lines.sql | 10 +++
dbt/models/housing_units_by_census_tracts.sql | 63 ------------------
dbt/models/parcels_distance_to_transit.sql | 9 +++
dbt/models/parcels_to_census_tracts.sql | 10 +++
.../property_values_by_census_tracts.sql | 22 -------
14 files changed, 262 insertions(+), 86 deletions(-)
create mode 100644 dbt/macros/median.sql
create mode 100644 dbt/macros/standardize.sql
create mode 100644 dbt/models/census_tracts_distance_to_transit.sql
create mode 100644 dbt/models/census_tracts_housing_units.sql
create mode 100644 dbt/models/census_tracts_parcel_area.sql
create mode 100644 dbt/models/census_tracts_property_values.sql
create mode 100644 dbt/models/census_tracts_wide.sql
delete mode 100644 dbt/models/housing_units_by_census_tracts.sql
delete mode 100644 dbt/models/property_values_by_census_tracts.sql
diff --git a/dbt/macros/median.sql b/dbt/macros/median.sql
new file mode 100644
index 00000000..131339f9
--- /dev/null
+++ b/dbt/macros/median.sql
@@ -0,0 +1,3 @@
+{% macro median(attr) %}
+(percentile_cont(0.5) within group (order by {{ attr }}))
+{% endmacro %}
diff --git a/dbt/macros/standardize.sql b/dbt/macros/standardize.sql
new file mode 100644
index 00000000..795ebad2
--- /dev/null
+++ b/dbt/macros/standardize.sql
@@ -0,0 +1,6 @@
+{% macro standardize(columns) %}
+ {% for c in columns %}
+ (({{ c }} - (avg({{ c }}) over ())) / (stddev_samp({{ c }}) over ())) as std_{{ c }}
+ {% if not loop.last %},{% endif %}
+ {% endfor %}
+{% endmacro %}
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index d3d8ac72..15783fa6 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -37,6 +37,7 @@ census_block_groups as (
, {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
, '[{{ year_ }}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
{% endif %}
+ , {{ year_ }} as year_
, st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_bg_500k') }}
@@ -67,5 +68,6 @@ select
, geoidfq
, census_tract_id
, valid
+ , year_
, geom
from census_block_groups_with_tracts
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 1119140c..31f09322 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -1,3 +1,13 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract_id'], 'unique': true},
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
with census_tracts as (
{% for year_ in var('census_years') %}
select
@@ -13,7 +23,8 @@ select
, tractce
, {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
, '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
-{% endif %}
+ {% endif %}
+ , {{ year_ }} as year_
, st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_tract_500k') }}
@@ -27,6 +38,7 @@ select
, tractce
, geoidfq
, valid
+ , year_
, geom
from
census_tracts
diff --git a/dbt/models/census_tracts_distance_to_transit.sql b/dbt/models/census_tracts_distance_to_transit.sql
new file mode 100644
index 00000000..8073ce5b
--- /dev/null
+++ b/dbt/models/census_tracts_distance_to_transit.sql
@@ -0,0 +1,36 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract_id'], 'unique': true},
+ ]
+ )
+}}
+
+with
+ parcels_distance_to_transit as (
+ select
+ parcel_id
+ , distance
+ from {{ ref('parcels_distance_to_transit') }}
+ )
+ , census_tracts as (
+ select
+ census_tract_id
+ from {{ ref('census_tracts') }}
+ )
+ , parcels_to_census_tracts as (
+ select
+ parcel_id
+ , census_tract_id
+ from {{ ref('parcels_to_census_tracts') }}
+ )
+select
+ census_tracts.census_tract_id
+ , avg(parcels_distance_to_transit.distance) as mean_distance_to_transit
+ , {{ median('parcels_distance_to_transit.distance') }} as median_distance_to_transit
+from
+ census_tracts
+ left join parcels_to_census_tracts using (census_tract_id)
+ left join parcels_distance_to_transit using (parcel_id)
+group by 1
diff --git a/dbt/models/census_tracts_housing_units.sql b/dbt/models/census_tracts_housing_units.sql
new file mode 100644
index 00000000..fe712f95
--- /dev/null
+++ b/dbt/models/census_tracts_housing_units.sql
@@ -0,0 +1,36 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract_id'], 'unique': true},
+ ]
+ )
+}}
+
+with census_tracts as (
+ select
+ census_tract_id
+ from {{ ref('census_tracts') }}
+)
+, residential_permits as (
+ select
+ residential_permit_id
+ , year_
+ , permit_value
+ , num_units
+ from {{ ref('residential_permits') }}
+)
+, residential_permits_to_census_tracts as (
+ select
+ residential_permit_id
+ , census_tract_id
+ from {{ ref('residential_permits_to_census_tracts') }}
+)
+select
+ census_tracts.census_tract_id
+ , sum(residential_permits.num_units) as num_units
+from
+ census_tracts
+ left join residential_permits_to_census_tracts using (census_tract_id)
+ left join residential_permits using (residential_permit_id)
+group by 1
diff --git a/dbt/models/census_tracts_parcel_area.sql b/dbt/models/census_tracts_parcel_area.sql
new file mode 100644
index 00000000..24d21cff
--- /dev/null
+++ b/dbt/models/census_tracts_parcel_area.sql
@@ -0,0 +1,34 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract_id'], 'unique': true},
+ ]
+ )
+}}
+
+with census_tracts as (
+ select
+ census_tract_id
+ from {{ ref('census_tracts') }}
+)
+, parcels as (
+ select
+ parcel_id
+ , geom
+ from {{ ref('parcels_base') }}
+)
+, parcels_to_census_tracts as (
+ select
+ parcel_id
+ , census_tract_id
+ from {{ ref('parcels_to_census_tracts') }}
+)
+select
+ census_tract_id
+ , sum(st_area(parcels.geom)) as parcel_sqm
+from
+ census_tracts
+ left join parcels_to_census_tracts using (census_tract_id)
+ left join parcels using (parcel_id)
+group by 1
diff --git a/dbt/models/census_tracts_property_values.sql b/dbt/models/census_tracts_property_values.sql
new file mode 100644
index 00000000..e2a4531b
--- /dev/null
+++ b/dbt/models/census_tracts_property_values.sql
@@ -0,0 +1,37 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract_id'], 'unique': true},
+ ]
+ )
+}}
+
+-- Median and total parcel property values aggregated by census tract.
+
+with parcels as (
+ select
+ parcel_id
+ , emv_total
+ from {{ ref('parcels_base') }}
+)
+, census_tracts as (
+ select
+ census_tract_id
+ from {{ ref('census_tracts') }}
+)
+, parcels_to_census_tracts as (
+ select
+ parcel_id
+ , census_tract_id
+ from {{ ref('parcels_to_census_tracts') }}
+)
+select
+ census_tracts.census_tract_id
+ , sum(parcels.emv_total) as total_value
+ , {{ median('parcels.emv_total') }} as median_value
+from
+ census_tracts
+ left join parcels_to_census_tracts using (census_tract_id)
+ left join parcels using (parcel_id)
+group by 1
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
new file mode 100644
index 00000000..adab9e44
--- /dev/null
+++ b/dbt/models/census_tracts_wide.sql
@@ -0,0 +1,66 @@
+with
+census_tracts as (
+ select
+ census_tract_id
+ , statefp || countyfp || tractce as census_tract
+ , year_
+ from {{ ref('census_tracts') }}
+)
+, census_tracts_housing_units as (
+ select
+ census_tract_id
+ , num_units
+ from {{ ref('census_tracts_housing_units') }}
+)
+, census_tracts_property_values as (
+ select
+ census_tract_id
+ , median_value
+ , total_value
+ from {{ ref('census_tracts_property_values') }}
+)
+, census_tracts_distance_to_transit as (
+ select
+ census_tract_id
+ , median_distance_to_transit
+ , mean_distance_to_transit
+ from {{ ref('census_tracts_distance_to_transit') }}
+)
+, census_tracts_parcel_area as (
+ select
+ census_tract_id
+ , parcel_sqm
+ from {{ ref('census_tracts_parcel_area') }}
+)
+, raw_data as (
+select
+ census_tracts.census_tract
+ , census_tracts.year_
+ , coalesce(census_tracts_housing_units.num_units, 0) as num_units
+ , census_tracts_property_values.total_value
+ , census_tracts_property_values.median_value
+ , census_tracts_distance_to_transit.median_distance_to_transit
+ , census_tracts_distance_to_transit.mean_distance_to_transit
+ , census_tracts_parcel_area.parcel_sqm
+from
+ census_tracts_housing_units
+ inner join census_tracts_property_values using(census_tract_id)
+ inner join census_tracts_distance_to_transit using (census_tract_id)
+ inner join census_tracts_parcel_area using (census_tract_id)
+ inner join census_tracts using (census_tract_id)
+where
+ census_tracts.year_ <= 2020
+ and census_tracts.census_tract_id in (select census_tract_id from {{ ref('census_tracts_in_city_boundary') }})
+)
+select
+ census_tract
+ , year_
+ , num_units
+ , total_value
+ , median_value
+ , median_distance_to_transit
+ , mean_distance_to_transit
+ , parcel_sqm
+ , {{ standardize(['num_units', 'total_value', 'median_value', 'median_distance_to_transit', 'mean_distance_to_transit', 'parcel_sqm']) }}
+from
+ raw_data
diff --git a/dbt/models/high_frequency_transit_lines.sql b/dbt/models/high_frequency_transit_lines.sql
index 34cd9779..42f13975 100644
--- a/dbt/models/high_frequency_transit_lines.sql
+++ b/dbt/models/high_frequency_transit_lines.sql
@@ -1,3 +1,13 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['high_frequency_transit_line_id'], 'unique': true},
+ {'columns': ['valid', 'geom'], 'type': 'gist'},
+ ]
+ )
+}}
+
with lines as (
select
valid
diff --git a/dbt/models/housing_units_by_census_tracts.sql b/dbt/models/housing_units_by_census_tracts.sql
deleted file mode 100644
index 208c12f3..00000000
--- a/dbt/models/housing_units_by_census_tracts.sql
+++ /dev/null
@@ -1,63 +0,0 @@
-with census_tracts as (
- select
- census_tract_id
- , statefp || countyfp || tractce as census_tract
- from {{ ref('census_tracts') }}
-)
-, parcels as (
- select
- parcel_id
- , geom
- from {{ ref('parcels') }}
-)
-, residential_permits as (
- select
- residential_permit_id
- , year_
- , permit_value
- , num_units
- from {{ ref('residential_permits') }}
-)
-, residential_permits_to_parcels as (
- select
- residential_permit_id
- , parcel_id
- from {{ ref('residential_permits_to_parcels') }}
-)
-, residential_permits_to_census_tracts as (
- select
- residential_permit_id
- , census_tract_id
- from {{ ref('residential_permits_to_census_tracts') }}
-)
-, residential as (
- select
- census_tracts.census_tract
- , residential_permits.year_
- , residential_permits.num_units
- , st_area(parcels.geom) as parcel_sqm
- , residential_permits.permit_value
- from
- residential_permits
- inner join residential_permits_to_parcels using (residential_permit_id)
- inner join parcels using (parcel_id)
- inner join residential_permits_to_census_tracts using (residential_permit_id)
- inner join census_tracts using (census_tract_id)
- where year_ <= 2020
-)
-, agg_residential as (
- select
- census_tract
- , year_
- , sum(num_units) as num_units
- from residential
- group by census_tract, year_
-)
-
-select
- census_tract
- , year_
- , num_units -- do we really want the total _applied_ units, or should we be
- -- looking at the total unit estimates from ACS?
-from
- agg_residential
diff --git a/dbt/models/parcels_distance_to_transit.sql b/dbt/models/parcels_distance_to_transit.sql
index c7881209..9edd1ff1 100644
--- a/dbt/models/parcels_distance_to_transit.sql
+++ b/dbt/models/parcels_distance_to_transit.sql
@@ -1,3 +1,12 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parcel_id'], 'unique': true},
+ ]
+ )
+}}
+
with
parcels as (
select
diff --git a/dbt/models/parcels_to_census_tracts.sql b/dbt/models/parcels_to_census_tracts.sql
index d3c2d6e0..4b742396 100644
--- a/dbt/models/parcels_to_census_tracts.sql
+++ b/dbt/models/parcels_to_census_tracts.sql
@@ -1,3 +1,13 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parcel_id'], 'unique': true},
+ {'columns': ['census_tract_id']}
+ ]
+ )
+}}
+
with
parcels as (
select
diff --git a/dbt/models/property_values_by_census_tracts.sql b/dbt/models/property_values_by_census_tracts.sql
deleted file mode 100644
index 51efa233..00000000
--- a/dbt/models/property_values_by_census_tracts.sql
+++ /dev/null
@@ -1,22 +0,0 @@
--- Median and total parcel property values aggregated by census tract.
-
-with parcels as (
- select
- parcel_id
- , emv_total
- from {{ ref('parcels_base') }}
-)
-, parcels_to_census_tracts as (
- select
- parcel_id
- , census_tract_id
- from {{ ref('parcels_to_census_tracts') }}
-)
-select
- parcels_to_census_tracts.census_tract_id
- , sum(parcels.emv_total) as total_value
- , percentile_cont(0.5) within group (order by parcels.emv_total) as median_value
-from
- parcels_to_census_tracts
- inner join parcels using (parcel_id)
-group by parcels_to_census_tracts.census_tract_id
From 13952dca56ae8967daae982c334e10e06eca4c0f Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 16:00:45 -0400
Subject: [PATCH 076/142] cleanup
---
dbt/models/acs_block_group.sql | 25 +------
dbt/models/acs_block_group_clean.sql | 14 +---
dbt/models/acs_tract.sql | 22 +-----
dbt/models/acs_tract_wide.sql | 25 ++-----
dbt/models/census_block_groups.sql | 24 ++-----
dbt/models/census_tracts.sql | 9 +--
.../census_tracts_distance_to_transit.sql | 14 +---
dbt/models/census_tracts_housing_units.sql | 16 +----
dbt/models/census_tracts_in_city_boundary.sql | 9 +--
dbt/models/census_tracts_parcel_area.sql | 14 +---
dbt/models/census_tracts_property_values.sql | 14 +---
dbt/models/census_tracts_wide.sql | 68 +++++++++----------
dbt/models/fair_market_rents.sql | 20 +-----
dbt/models/high_frequency_transit_lines.sql | 14 +---
dbt/models/parcels_base.sql | 13 +---
dbt/models/parcels_to_census_tracts.sql | 18 +----
dbt/models/segregation_indexes.sql | 20 +-----
dbt/models/usps_migration.sql | 12 +---
18 files changed, 74 insertions(+), 277 deletions(-)
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index 6165c8ac..37d6f96e 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -8,29 +8,8 @@
}}
with
-census_block_groups as (
- select
- census_block_group_id
- , statefp
- , countyfp
- , tractce
- , blkgrpce
- , valid
- from
- {{ ref('census_block_groups') }}
-)
-, acs_bg as (
- select
- statefp
- , countyfp
- , tractce
- , blkgrpce
- , year_
- , name_
- , value_
- from
- {{ ref('acs_block_group_clean') }}
-)
+census_block_groups as (select * from {{ ref('census_block_groups') }})
+, acs_bg as (select * from {{ ref('acs_block_group_clean') }})
select
census_block_groups.census_block_group_id
, acs_bg.year_
diff --git a/dbt/models/acs_block_group_clean.sql b/dbt/models/acs_block_group_clean.sql
index d629d782..22cf94e4 100644
--- a/dbt/models/acs_block_group_clean.sql
+++ b/dbt/models/acs_block_group_clean.sql
@@ -1,15 +1,3 @@
-with
-acs_bg_raw as (
- select
- statefp
- , countyfp
- , tractce
- , blkgrpce
- , year
- , code
- , value
- from {{ source('minneapolis', 'acs_bg_raw') }}
-)
select
statefp
, countyfp
@@ -19,4 +7,4 @@ select
, code as name_
, case when "value" < 0 then null else "value" end as value_
from
- acs_bg_raw
+ {{ source('minneapolis', 'acs_bg_raw') }}
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index 7482f43a..3909b2dc 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -8,26 +8,8 @@
}}
with
-census_tracts as (
- select
- census_tract_id
- , statefp
- , countyfp
- , tractce
- , valid
-
- from {{ ref("census_tracts") }}
-)
-, acs_tract as (
- select
- statefp
- , countyfp
- , tractce
- , year_
- , name_
- , value_
- from {{ ref('acs_tract_clean') }}
-)
+census_tracts as (select * from {{ ref("census_tracts") }})
+, acs_tract as (select * from {{ ref('acs_tract_clean') }})
select
census_tract_id
, acs_tract.year_
diff --git a/dbt/models/acs_tract_wide.sql b/dbt/models/acs_tract_wide.sql
index 434d2d9e..0d142795 100644
--- a/dbt/models/acs_tract_wide.sql
+++ b/dbt/models/acs_tract_wide.sql
@@ -9,21 +9,12 @@
{% set years = range(2013, 2023) %}
-with acs_tract as (
- select
- census_tract_id
- , year_
- , name_
- , value_
- from {{ ref('acs_tract') }}
-)
-
+with
+acs_tract as (select * from {{ ref('acs_tract') }})
+, acs_variables as (select * from {{ ref("acs_variables") }})
, census_tracts_in_city_boundary as (
- select
- census_tract_id
- from {{ ref("census_tracts_in_city_boundary") }}
+ select * from {{ ref("census_tracts_in_city_boundary") }}
)
-
, census_tracts as (
select
census_tract_id
@@ -31,14 +22,6 @@ with acs_tract as (
from {{ ref("census_tracts") }}
where census_tract_id in (select census_tract_id from census_tracts_in_city_boundary)
)
-
-, acs_variables as (
- select
- "variable"
- , description
- from {{ ref("acs_variables") }}
-)
-
, acs_tract_extended as (
select
acs_tract.census_tract_id
diff --git a/dbt/models/census_block_groups.sql b/dbt/models/census_block_groups.sql
index 15783fa6..b33a6aea 100644
--- a/dbt/models/census_block_groups.sql
+++ b/dbt/models/census_block_groups.sql
@@ -10,15 +10,7 @@
}}
with
-census_tracts as (
- select
- census_tract_id
- , statefp
- , countyfp
- , tractce
- , valid
- from {{ ref("census_tracts") }}
-),
+census_tracts as (select * from {{ ref("census_tracts") }}),
census_block_groups as (
{% for year_ in var('census_years') %}
select
@@ -55,19 +47,11 @@ census_block_groups_with_tracts as (
, (census_block_groups.valid * census_tracts.valid) as valid
, census_block_groups.geom
from census_block_groups
- inner join census_tracts using (statefp , countyfp , tractce)
+ inner join census_tracts using (statefp, countyfp, tractce)
where
census_tracts.valid && census_block_groups.valid
)
select
- {{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_block_group_id
- , statefp
- , countyfp
- , tractce
- , blkgrpce
- , geoidfq
- , census_tract_id
- , valid
- , year_
- , geom
+ {{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_block_group_id,
+ *
from census_block_groups_with_tracts
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 31f09322..c6a67620 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -32,13 +32,6 @@ from
{% endfor %}
)
select
- {{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_tract_id
- , statefp
- , countyfp
- , tractce
- , geoidfq
- , valid
- , year_
- , geom
+ {{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_tract_id, *
from
census_tracts
diff --git a/dbt/models/census_tracts_distance_to_transit.sql b/dbt/models/census_tracts_distance_to_transit.sql
index 8073ce5b..2c905572 100644
--- a/dbt/models/census_tracts_distance_to_transit.sql
+++ b/dbt/models/census_tracts_distance_to_transit.sql
@@ -9,21 +9,13 @@
with
parcels_distance_to_transit as (
- select
- parcel_id
- , distance
- from {{ ref('parcels_distance_to_transit') }}
+ select * from {{ ref('parcels_distance_to_transit') }}
)
, census_tracts as (
- select
- census_tract_id
- from {{ ref('census_tracts') }}
+ select * from {{ ref('census_tracts') }}
)
, parcels_to_census_tracts as (
- select
- parcel_id
- , census_tract_id
- from {{ ref('parcels_to_census_tracts') }}
+ select * from {{ ref('parcels_to_census_tracts') }}
)
select
census_tracts.census_tract_id
diff --git a/dbt/models/census_tracts_housing_units.sql b/dbt/models/census_tracts_housing_units.sql
index fe712f95..c60779e2 100644
--- a/dbt/models/census_tracts_housing_units.sql
+++ b/dbt/models/census_tracts_housing_units.sql
@@ -8,23 +8,13 @@
}}
with census_tracts as (
- select
- census_tract_id
- from {{ ref('census_tracts') }}
+ select * from {{ ref('census_tracts') }}
)
, residential_permits as (
- select
- residential_permit_id
- , year_
- , permit_value
- , num_units
- from {{ ref('residential_permits') }}
+ select * from {{ ref('residential_permits') }}
)
, residential_permits_to_census_tracts as (
- select
- residential_permit_id
- , census_tract_id
- from {{ ref('residential_permits_to_census_tracts') }}
+ select * from {{ ref('residential_permits_to_census_tracts') }}
)
select
census_tracts.census_tract_id
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
index 51e1d4e2..be4771e3 100644
--- a/dbt/models/census_tracts_in_city_boundary.sql
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -1,13 +1,8 @@
with census_tracts as (
- select
- census_tract_id
- , geom
- from {{ ref('census_tracts') }}
+ select * from {{ ref('census_tracts') }}
)
, city_boundary as (
- select
- geom
- from {{ ref('city_boundary') }}
+ select * from {{ ref('city_boundary') }}
)
select
census_tracts.census_tract_id
diff --git a/dbt/models/census_tracts_parcel_area.sql b/dbt/models/census_tracts_parcel_area.sql
index 24d21cff..4751c4ea 100644
--- a/dbt/models/census_tracts_parcel_area.sql
+++ b/dbt/models/census_tracts_parcel_area.sql
@@ -8,21 +8,13 @@
}}
with census_tracts as (
- select
- census_tract_id
- from {{ ref('census_tracts') }}
+ select * from {{ ref('census_tracts') }}
)
, parcels as (
- select
- parcel_id
- , geom
- from {{ ref('parcels_base') }}
+ select * from {{ ref('parcels_base') }}
)
, parcels_to_census_tracts as (
- select
- parcel_id
- , census_tract_id
- from {{ ref('parcels_to_census_tracts') }}
+ select * from {{ ref('parcels_to_census_tracts') }}
)
select
census_tract_id
diff --git a/dbt/models/census_tracts_property_values.sql b/dbt/models/census_tracts_property_values.sql
index e2a4531b..0b140a92 100644
--- a/dbt/models/census_tracts_property_values.sql
+++ b/dbt/models/census_tracts_property_values.sql
@@ -10,21 +10,13 @@
-- Median and total parcel property values aggregated by census tract.
with parcels as (
- select
- parcel_id
- , emv_total
- from {{ ref('parcels_base') }}
+ select * from {{ ref('parcels_base') }}
)
, census_tracts as (
- select
- census_tract_id
- from {{ ref('census_tracts') }}
+ select * from {{ ref('census_tracts') }}
)
, parcels_to_census_tracts as (
- select
- parcel_id
- , census_tract_id
- from {{ ref('parcels_to_census_tracts') }}
+ select * from {{ ref('parcels_to_census_tracts') }}
)
select
census_tracts.census_tract_id
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index adab9e44..49433109 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -1,57 +1,49 @@
with
-census_tracts as (
+census_tracts_in_city_boundary as (
+ select
+ census_tract_id
+ from {{ ref('census_tracts_in_city_boundary') }}
+)
+, census_tracts as (
select
census_tract_id
, statefp || countyfp || tractce as census_tract
, year_
from {{ ref('census_tracts') }}
+ where
+ year_ <= 2020
+ and census_tract_id in (select * from census_tracts_in_city_boundary)
)
-, census_tracts_housing_units as (
- select
- census_tract_id
- , num_units
- from {{ ref('census_tracts_housing_units') }}
+, housing_units as (
+ select * from {{ ref('census_tracts_housing_units') }}
)
-, census_tracts_property_values as (
- select
- census_tract_id
- , median_value
- , total_value
- from {{ ref('census_tracts_property_values') }}
+, property_values as (
+ select * from {{ ref('census_tracts_property_values') }}
)
-, census_tracts_distance_to_transit as (
- select
- census_tract_id
- , median_distance_to_transit
- , mean_distance_to_transit
- from {{ ref('census_tracts_distance_to_transit') }}
+, distance_to_transit as (
+ select * from {{ ref('census_tracts_distance_to_transit') }}
)
-, census_tracts_parcel_area as (
- select
- census_tract_id
- , parcel_sqm
- from {{ ref('census_tracts_parcel_area') }}
+, parcel_area as (
+ select * from {{ ref('census_tracts_parcel_area') }}
)
, raw_data as (
select
census_tracts.census_tract
, census_tracts.year_
- , coalesce(census_tracts_housing_units.num_units, 0) as num_units
- , census_tracts_property_values.total_value
- , census_tracts_property_values.median_value
- , census_tracts_distance_to_transit.median_distance_to_transit
- , census_tracts_distance_to_transit.mean_distance_to_transit
- , census_tracts_parcel_area.parcel_sqm
+ , coalesce(housing_units.num_units, 0) as num_units
+ , property_values.total_value
+ , property_values.median_value
+ , distance_to_transit.median_distance_to_transit
+ , distance_to_transit.mean_distance_to_transit
+ , parcel_area.parcel_sqm
from
- census_tracts_housing_units
- inner join census_tracts_property_values using(census_tract_id)
- inner join census_tracts_distance_to_transit using (census_tract_id)
- inner join census_tracts_parcel_area using (census_tract_id)
- inner join census_tracts using (census_tract_id)
-where
- census_tracts.year_ <= 2020
- and census_tracts.census_tract_id in (select census_tract_id from {{ ref('census_tracts_in_city_boundary') }})
+ census_tracts
+ inner join housing_units using (census_tract_id)
+ inner join property_values using (census_tract_id)
+ inner join distance_to_transit using (census_tract_id)
+ inner join parcel_area using (census_tract_id)
)
+, with_std as (
select
census_tract
, year_
@@ -64,3 +56,5 @@ select
, {{ standardize(['num_units', 'total_value', 'median_value', 'median_distance_to_transit', 'mean_distance_to_transit', 'parcel_sqm']) }}
from
raw_data
+)
+select * from with_std
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index 9927b36f..a9a9cdbc 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -1,24 +1,8 @@
{% set num_bedrooms = range(0, 5) %}
with
-zip_codes as (
- select
- zip_code_id
- , zip_code
- , valid
- from {{ ref('zip_codes') }}
-)
-, fair_market_rents as (
- select
- zip_code
- , rent_br0
- , rent_br1
- , rent_br2
- , rent_br3
- , rent_br4
- , year_
- from {{ ref('fair_market_rents_union') }}
-)
+zip_codes as (select * from {{ ref('zip_codes') }})
+, fair_market_rents as (select * from {{ ref('fair_market_rents_union') }})
, fmr_zip as (
select
zip_codes.zip_code_id
diff --git a/dbt/models/high_frequency_transit_lines.sql b/dbt/models/high_frequency_transit_lines.sql
index 42f13975..34d1238a 100644
--- a/dbt/models/high_frequency_transit_lines.sql
+++ b/dbt/models/high_frequency_transit_lines.sql
@@ -8,18 +8,8 @@
)
}}
-with lines as (
- select
- valid
- , geom
- from {{ ref('high_frequency_transit_lines_union') }}
-)
-, stops as (
- select
- valid
- , geom
- from {{ ref('high_frequency_transit_stops') }}
-)
+with lines as (select * from {{ ref('high_frequency_transit_lines_union') }})
+, stops as (select * from {{ ref('high_frequency_transit_stops') }})
, lines_and_stops as (
select
lines.valid * stops.valid as valid
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 6fb778f1..29055aa4 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -24,21 +24,12 @@ with parcels as (
nullif(year_built, 0) as year_built,
sale_date,
nullif(sale_value, 0) as sale_value,
- geom
+ st_transform(geom, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'parcels_shp_plan_regonal_' ~ year_ ~ '_parcels' ~ year_ ~ 'hennepin') }}
where upper({{ "city" if year_ < 2018 else "ctu_name" }}) = '{{ city }}'
{% if not loop.last %}union all{% endif %}
{% endfor %}
)
select
- {{ dbt_utils.generate_surrogate_key(['ogc_fid', 'valid']) }} as parcel_id
- , pin
- , valid
- , emv_land
- , emv_bldg
- , emv_total
- , year_built
- , sale_date
- , sale_value
- , st_transform(geom, {{ var("srid") }}) as geom
+ {{ dbt_utils.generate_surrogate_key(['ogc_fid', 'valid']) }} as parcel_id, *
from parcels
diff --git a/dbt/models/parcels_to_census_tracts.sql b/dbt/models/parcels_to_census_tracts.sql
index 4b742396..75f5f360 100644
--- a/dbt/models/parcels_to_census_tracts.sql
+++ b/dbt/models/parcels_to_census_tracts.sql
@@ -9,22 +9,10 @@
}}
with
-parcels as (
- select
- parcel_id
- from {{ ref("parcels_base") }}
-)
-, census_block_groups as (
- select
- census_block_group_id
- , census_tract_id
- from {{ ref("census_block_groups") }}
-)
+parcels as (select * from {{ ref("parcels_base") }})
+, census_block_groups as (select * from {{ ref("census_block_groups") }})
, parcels_to_census_block_groups as (
- select
- parcel_id
- , census_block_group_id
- from {{ ref("parcels_to_census_block_groups") }}
+ select * from {{ ref("parcels_to_census_block_groups") }}
)
select
parcels.parcel_id
diff --git a/dbt/models/segregation_indexes.sql b/dbt/models/segregation_indexes.sql
index 8d50587f..206545e5 100644
--- a/dbt/models/segregation_indexes.sql
+++ b/dbt/models/segregation_indexes.sql
@@ -6,23 +6,9 @@
-- has many more white people than the average for the city will have a high
-- segregation index for the 'average_city' distribution.
with
- categories as (
- select category from {{ ref("population_categories") }}
- )
- , acs_tract as (
- select
- census_tract_id
- , year_
- , name_
- , value_
- from {{ ref("acs_tract") }}
- )
- , acs_variables as (
- select
- "variable" as name_
- , description
- from {{ ref("acs_variables") }}
- )
+ categories as (select * from {{ ref("population_categories") }})
+ , acs_tract as (select * from {{ ref("acs_tract") }})
+ , acs_variables as (select * from {{ ref("acs_variables") }})
, pop_tyc as
( -- Population by tract, year, and category
select acs_tract.census_tract_id, acs_tract.year_, categories.category, acs_tract.value_
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 7550f0d0..2cafef6c 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -10,18 +10,12 @@
{% set usps_migration_flow_types = ['business', 'family', 'individual', 'perm', 'temp'] %}
{% set usps_migration_flow_directions = ['from', 'to'] %}
-with process_date as (
+with
+zip_codes as (select * from {{ ref('zip_codes') }})
+, process_date as (
select to_date(yyyy_mm, 'YYYYMM') as date_, *
from {{ ref('usps_migration_union') }}
)
-, zip_codes as (
- select
- zip_code_id
- , zip_code
- , valid
- from
- {{ ref('zip_codes') }}
-)
, add_zip_id as (
select zip_code_id, process_date.*
from
From 92f6285d8070c05d062ca57f5c51770978f3311d Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 17:28:11 -0400
Subject: [PATCH 077/142] finish adding columns to census_tracts_wide
---
.../census_tracts_distance_to_transit.sql | 10 ++--
dbt/models/census_tracts_parcel_area.sql | 15 ++----
dbt/models/census_tracts_parking_limits.sql | 50 +++++++++++++++++++
dbt/models/census_tracts_property_values.sql | 15 ++----
dbt/models/census_tracts_wide.sql | 33 ++++++------
dbt/models/commercial_permits_to_parcels.sql | 2 +-
dbt/models/parcels.sql | 40 ++++++++-------
dbt/models/parcels_distance_to_transit.sql | 15 +-----
dbt/models/parcels_to_census_tracts.sql | 23 ---------
dbt/models/residential_permits_to_parcels.sql | 2 +-
dbt/models/segregation_indexes.sql | 7 ++-
11 files changed, 108 insertions(+), 104 deletions(-)
create mode 100644 dbt/models/census_tracts_parking_limits.sql
delete mode 100644 dbt/models/parcels_to_census_tracts.sql
diff --git a/dbt/models/census_tracts_distance_to_transit.sql b/dbt/models/census_tracts_distance_to_transit.sql
index 2c905572..edf28211 100644
--- a/dbt/models/census_tracts_distance_to_transit.sql
+++ b/dbt/models/census_tracts_distance_to_transit.sql
@@ -11,18 +11,14 @@ with
parcels_distance_to_transit as (
select * from {{ ref('parcels_distance_to_transit') }}
)
- , census_tracts as (
- select * from {{ ref('census_tracts') }}
- )
- , parcels_to_census_tracts as (
- select * from {{ ref('parcels_to_census_tracts') }}
- )
+ , census_tracts as (select * from {{ ref('census_tracts') }})
+ , parcels as (select * from {{ ref('parcels') }})
select
census_tracts.census_tract_id
, avg(parcels_distance_to_transit.distance) as mean_distance_to_transit
, {{ median('parcels_distance_to_transit.distance') }} as median_distance_to_transit
from
census_tracts
- left join parcels_to_census_tracts using (census_tract_id)
+ left join parcels using (census_tract_id)
left join parcels_distance_to_transit using (parcel_id)
group by 1
diff --git a/dbt/models/census_tracts_parcel_area.sql b/dbt/models/census_tracts_parcel_area.sql
index 4751c4ea..687d2274 100644
--- a/dbt/models/census_tracts_parcel_area.sql
+++ b/dbt/models/census_tracts_parcel_area.sql
@@ -7,20 +7,13 @@
)
}}
-with census_tracts as (
- select * from {{ ref('census_tracts') }}
-)
-, parcels as (
- select * from {{ ref('parcels_base') }}
-)
-, parcels_to_census_tracts as (
- select * from {{ ref('parcels_to_census_tracts') }}
-)
+with
+census_tracts as (select * from {{ ref('census_tracts') }}),
+parcels as (select * from {{ ref('parcels') }})
select
census_tract_id
, sum(st_area(parcels.geom)) as parcel_sqm
from
census_tracts
- left join parcels_to_census_tracts using (census_tract_id)
- left join parcels using (parcel_id)
+ left join parcels using (census_tract_id)
group by 1
diff --git a/dbt/models/census_tracts_parking_limits.sql b/dbt/models/census_tracts_parking_limits.sql
new file mode 100644
index 00000000..b40f89ea
--- /dev/null
+++ b/dbt/models/census_tracts_parking_limits.sql
@@ -0,0 +1,50 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract_id'], 'unique': true},
+ ]
+ )
+}}
+
+with
+parcels as (select * from {{ ref('parcels') }}),
+transit as (select * from {{ ref('high_frequency_transit_lines') }}),
+with_parking_limit as (
+ select
+ parcel_id,
+ census_tract_id,
+ case
+ when parcels.valid << '[2015-01-01,)'::daterange then 'full'
+ else
+ case
+ when st_intersects(parcels.geom, transit.blue_zone_geom) then 'eliminated'
+ when st_intersects(parcels.geom, transit.yellow_zone_geom) then 'reduced'
+ else 'full'
+ end
+ end as limit_
+ from
+ parcels
+ left join transit
+ on parcels.valid && transit.valid
+),
+with_limit_numeric as (
+ select
+ parcel_id,
+ census_tract_id,
+ limit_,
+ case limit_
+ when 'full' then 1
+ when 'reduced' then 0.5
+ when 'eliminated' then 0
+ end as limit_numeric
+ from with_parking_limit
+),
+by_census_tract as (
+ select
+ census_tract_id,
+ avg(limit_numeric) as mean_limit
+ from with_limit_numeric
+ group by census_tract_id
+)
+select * from by_census_tract
diff --git a/dbt/models/census_tracts_property_values.sql b/dbt/models/census_tracts_property_values.sql
index 0b140a92..7bb18e72 100644
--- a/dbt/models/census_tracts_property_values.sql
+++ b/dbt/models/census_tracts_property_values.sql
@@ -9,21 +9,14 @@
-- Median and total parcel property values aggregated by census tract.
-with parcels as (
- select * from {{ ref('parcels_base') }}
-)
-, census_tracts as (
- select * from {{ ref('census_tracts') }}
-)
-, parcels_to_census_tracts as (
- select * from {{ ref('parcels_to_census_tracts') }}
-)
+with
+parcels as (select * from {{ ref('parcels') }})
+, census_tracts as (select * from {{ ref('census_tracts') }})
select
census_tracts.census_tract_id
, sum(parcels.emv_total) as total_value
, {{ median('parcels.emv_total') }} as median_value
from
census_tracts
- left join parcels_to_census_tracts using (census_tract_id)
- left join parcels using (parcel_id)
+ left join parcels using (census_tract_id)
group by 1
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index 49433109..d080f37e 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -1,9 +1,16 @@
+{{
+ config(
+ materialized='table',
+ )
+}}
+
with
-census_tracts_in_city_boundary as (
- select
- census_tract_id
- from {{ ref('census_tracts_in_city_boundary') }}
-)
+in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
+, housing_units as (select * from {{ ref('census_tracts_housing_units') }})
+, property_values as (select * from {{ ref('census_tracts_property_values') }})
+, distance_to_transit as (select * from {{ ref('census_tracts_distance_to_transit') }})
+, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
+, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
, census_tracts as (
select
census_tract_id
@@ -12,19 +19,7 @@ census_tracts_in_city_boundary as (
from {{ ref('census_tracts') }}
where
year_ <= 2020
- and census_tract_id in (select * from census_tracts_in_city_boundary)
-)
-, housing_units as (
- select * from {{ ref('census_tracts_housing_units') }}
-)
-, property_values as (
- select * from {{ ref('census_tracts_property_values') }}
-)
-, distance_to_transit as (
- select * from {{ ref('census_tracts_distance_to_transit') }}
-)
-, parcel_area as (
- select * from {{ ref('census_tracts_parcel_area') }}
+ and census_tract_id in (select census_tract_id from in_city_boundary)
)
, raw_data as (
select
@@ -36,12 +31,14 @@ select
, distance_to_transit.median_distance_to_transit
, distance_to_transit.mean_distance_to_transit
, parcel_area.parcel_sqm
+ , parking_limits.mean_limit
from
census_tracts
inner join housing_units using (census_tract_id)
inner join property_values using (census_tract_id)
inner join distance_to_transit using (census_tract_id)
inner join parcel_area using (census_tract_id)
+ inner join parking_limits using (census_tract_id)
)
, with_std as (
select
diff --git a/dbt/models/commercial_permits_to_parcels.sql b/dbt/models/commercial_permits_to_parcels.sql
index de1df444..b74a47f4 100644
--- a/dbt/models/commercial_permits_to_parcels.sql
+++ b/dbt/models/commercial_permits_to_parcels.sql
@@ -21,7 +21,7 @@ commercial_permits as (
parcel_id as id
, valid
, geom
- from {{ ref("parcels_base") }}
+ from {{ ref("parcels") }}
)
select
child_id as commercial_permit_id
diff --git a/dbt/models/parcels.sql b/dbt/models/parcels.sql
index f3482927..12a48c54 100644
--- a/dbt/models/parcels.sql
+++ b/dbt/models/parcels.sql
@@ -1,21 +1,25 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parcel_id'], 'unique': true},
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
with
-parcels_to_zip_codes as (
- select
- parcel_id
- , zip_code_id
- from {{ref('parcels_to_zip_codes')}}
-),
-parcels_to_census_block_groups as (
- select
- parcel_id
- , census_block_group_id
- from {{ref('parcels_to_census_block_groups')}}
-)
+parcels as (select * from {{ ref('parcels_base') }}),
+to_zip_codes as (select * from {{ref('parcels_to_zip_codes')}}),
+to_census_bgs as (select * from {{ref('parcels_to_census_block_groups')}}),
+census_bgs as (select * from {{ref('census_block_groups')}})
select
- {{ dbt_utils.star(ref('parcels_base')) }}
- , zip_code_id
- , census_block_group_id
+ parcels.*
+ , to_zip_codes.zip_code_id
+ , to_census_bgs.census_block_group_id
+ , census_bgs.census_tract_id
from
- {{ ref('parcels_base') }}
- left join parcels_to_zip_codes using (parcel_id)
- left join parcels_to_census_block_groups using (parcel_id)
+ parcels
+ left join to_zip_codes using (parcel_id)
+ left join to_census_bgs using (parcel_id)
+ left join census_bgs using (census_block_group_id)
diff --git a/dbt/models/parcels_distance_to_transit.sql b/dbt/models/parcels_distance_to_transit.sql
index 9edd1ff1..77a5daf6 100644
--- a/dbt/models/parcels_distance_to_transit.sql
+++ b/dbt/models/parcels_distance_to_transit.sql
@@ -8,19 +8,8 @@
}}
with
- parcels as (
- select
- parcel_id
- , valid
- , geom
- from {{ ref('parcels_base') }}
- )
- , high_frequency_transit_lines as (
- select
- valid
- , geom
- from {{ ref('high_frequency_transit_lines') }}
- )
+ parcels as (select * from {{ ref('parcels') }})
+ , high_freq_transit as (select * from {{ ref('high_frequency_transit_lines') }})
select
parcels.parcel_id
, st_distance(parcels.geom, high_frequency_transit_lines.geom) as distance
diff --git a/dbt/models/parcels_to_census_tracts.sql b/dbt/models/parcels_to_census_tracts.sql
deleted file mode 100644
index 75f5f360..00000000
--- a/dbt/models/parcels_to_census_tracts.sql
+++ /dev/null
@@ -1,23 +0,0 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['parcel_id'], 'unique': true},
- {'columns': ['census_tract_id']}
- ]
- )
-}}
-
-with
-parcels as (select * from {{ ref("parcels_base") }})
-, census_block_groups as (select * from {{ ref("census_block_groups") }})
-, parcels_to_census_block_groups as (
- select * from {{ ref("parcels_to_census_block_groups") }}
-)
-select
- parcels.parcel_id
- , census_block_groups.census_tract_id
-from
- parcels
- left join parcels_to_census_block_groups using (parcel_id)
- left join census_block_groups using (census_block_group_id)
diff --git a/dbt/models/residential_permits_to_parcels.sql b/dbt/models/residential_permits_to_parcels.sql
index 7f9ea59c..daedfab1 100644
--- a/dbt/models/residential_permits_to_parcels.sql
+++ b/dbt/models/residential_permits_to_parcels.sql
@@ -21,7 +21,7 @@ residential_permits as (
parcel_id as id
, valid
, geom
- from {{ ref("parcels_base") }}
+ from {{ ref("parcels") }}
)
select
child_id as residential_permit_id
diff --git a/dbt/models/segregation_indexes.sql b/dbt/models/segregation_indexes.sql
index 206545e5..ea47ba0f 100644
--- a/dbt/models/segregation_indexes.sql
+++ b/dbt/models/segregation_indexes.sql
@@ -8,7 +8,12 @@
with
categories as (select * from {{ ref("population_categories") }})
, acs_tract as (select * from {{ ref("acs_tract") }})
- , acs_variables as (select * from {{ ref("acs_variables") }})
+ , acs_variables as (
+ select
+ variable as name_,
+ description
+ from {{ ref("acs_variables") }}
+ )
, pop_tyc as
( -- Population by tract, year, and category
select acs_tract.census_tract_id, acs_tract.year_, categories.category, acs_tract.value_
From 2526c3188369b4323df2167ace9148855b9953bb Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 21 Aug 2024 17:46:31 -0400
Subject: [PATCH 078/142] remove unused line in clean.sh
---
scripts/clean.sh | 2 --
1 file changed, 2 deletions(-)
diff --git a/scripts/clean.sh b/scripts/clean.sh
index 2bd06083..bf0eccb4 100755
--- a/scripts/clean.sh
+++ b/scripts/clean.sh
@@ -8,5 +8,3 @@ autoflake --remove-all-unused-imports --in-place --recursive ./cities ./tests
nbqa black docs/guides/
nbqa autoflake --remove-all-unused-imports --recursive --in-place docs/guides/
nbqa isort -in-place docs/guides/
-
-pg_format -c .pg_format -i etl/*.sql
From 8da0487a379a964110206031ee72192a3292b11b Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 22 Aug 2024 14:14:40 -0400
Subject: [PATCH 079/142] switch api to use public schema
---
api/schema.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/schema.sql b/api/schema.sql
index 8578cdbf..7c639ef9 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -3,7 +3,7 @@ drop schema if exists api cascade;
create schema api;
create view api.acs_tract_wide as (
- select * from dbt.acs_tract_wide
+ select * from acs_tract_wide
order by random()
);
From 9319009a15f4d1befb00085a726d1a8a7089efff Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 22 Aug 2024 17:41:24 -0400
Subject: [PATCH 080/142] distance to transit should include lines and stops
---
dbt/models/high_frequency_transit_stops.sql | 5 +++--
dbt/models/parcels_distance_to_transit.sql | 17 +++++++++++++----
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/dbt/models/high_frequency_transit_stops.sql b/dbt/models/high_frequency_transit_stops.sql
index 9d9a0459..38f40aa0 100644
--- a/dbt/models/high_frequency_transit_stops.sql
+++ b/dbt/models/high_frequency_transit_stops.sql
@@ -1,9 +1,10 @@
with stops_2015 as (
select
- st_union(st_transform(geom, {{ var("srid") }}))::geometry(multipoint, {{ var("srid") }}) as geom
+ st_union(st_transform(geom, {{ var("srid") }})) as geom
from {{ source('minneapolis', 'high_frequency_transit_2015_freq_rail_stops') }}
)
select
- '[,]'::daterange as valid
+ 0 as high_frequency_transit_stop_id
+ , '[,]'::daterange as valid
, geom
from stops_2015
diff --git a/dbt/models/parcels_distance_to_transit.sql b/dbt/models/parcels_distance_to_transit.sql
index 77a5daf6..6580ca58 100644
--- a/dbt/models/parcels_distance_to_transit.sql
+++ b/dbt/models/parcels_distance_to_transit.sql
@@ -7,13 +7,22 @@
)
}}
+-- This model calculates the distance from each parcel to the nearest high
+-- frequency transit line or stop
with
parcels as (select * from {{ ref('parcels') }})
- , high_freq_transit as (select * from {{ ref('high_frequency_transit_lines') }})
+ , lines as (select * from {{ ref('high_frequency_transit_lines') }})
+ , stops as (select * from {{ ref('high_frequency_transit_stops') }})
+ , lines_and_stops as materialized (
+ select
+ lines.valid * stops.valid as valid
+ , st_union(lines.geom, stops.geom) as geom
+ from
+ lines inner join stops on lines.valid && stops.valid
+)
select
parcels.parcel_id
- , st_distance(parcels.geom, high_frequency_transit_lines.geom) as distance
+ , st_distance(parcels.geom, lines_and_stops.geom) as distance
from
parcels
- inner join high_frequency_transit_lines
- on parcels.valid && high_frequency_transit_lines.valid
+ inner join lines_and_stops on parcels.valid && lines_and_stops.valid
From d35316799103ac627601341f5d8d497e5184be57 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 22 Aug 2024 17:41:47 -0400
Subject: [PATCH 081/142] correct year range for parcels
---
dbt/models/parcels_base.sql | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 29055aa4..4e7a6fbb 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -12,24 +12,35 @@
{% set city = 'MINNEAPOLIS' %}
{% set county_id = '053' %}
-with parcels as (
+with
+-- This is a union of all the parcels from the years 2002 to 2023
+parcels_union as (
{% for year_ in years %}
select
ogc_fid,
replace(pin, '{{ county_id }}-', '') as pin,
- '[{{ year_ - 1 }}-01-01,{{ year_ }}-01-01)'::daterange as valid,
+
+ -- parcels are a year-end snapshot, named after the year they cover
+ '[{{ year_ }}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid,
nullif(emv_land, 0) as emv_land,
nullif(emv_bldg, 0) as emv_bldg,
nullif(emv_total, 0) as emv_total,
nullif(year_built, 0) as year_built,
- sale_date,
+ nullif(sale_date, '1899-12-30'::date),
nullif(sale_value, 0) as sale_value,
st_transform(geom, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'parcels_shp_plan_regonal_' ~ year_ ~ '_parcels' ~ year_ ~ 'hennepin') }}
where upper({{ "city" if year_ < 2018 else "ctu_name" }}) = '{{ city }}'
{% if not loop.last %}union all{% endif %}
{% endfor %}
+),
+
+-- Some of the parcel datasets contain exact duplicates that we remove. Note
+-- that duplicate pin/year pairs may remain.
+parcels_distinct as (
+ select distinct on (pin, valid, emv_land, emv_bldg, emv_total, year_built, sale_date, sale_value, geom) *
+ from parcels_union
)
select
{{ dbt_utils.generate_surrogate_key(['ogc_fid', 'valid']) }} as parcel_id, *
-from parcels
+from parcels_distinct
From 24af31a06eb1f6b70acd87110266fcfc17513b67 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 22 Aug 2024 17:42:03 -0400
Subject: [PATCH 082/142] add census_tract field
---
dbt/models/census_tracts.sql | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index c6a67620..4ffe0004 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -8,7 +8,7 @@
)
}}
-with census_tracts as (
+with census_tracts_union as (
{% for year_ in var('census_years') %}
select
{% if year_ == 2010 %}
@@ -30,8 +30,12 @@ from
{{ source('minneapolis', 'census_cb_' ~ year_ ~ '_27_tract_500k') }}
{% if not loop.last %}union all{% endif %}
{% endfor %}
+),
+with_census_tract as (
+ select *, statefp || countyfp || tractce as census_tract
+ from census_tracts_union
)
select
{{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_tract_id, *
from
- census_tracts
+ with_census_tract
From 5a00c7ffcfe3de02e03c7fd893d966bb654ee0f4 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 22 Aug 2024 17:42:12 -0400
Subject: [PATCH 083/142] add acs fields to census_tract_wide
---
dbt/models/census_tracts_wide.sql | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index d080f37e..e078af39 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -12,15 +12,20 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
, census_tracts as (
- select
- census_tract_id
- , statefp || countyfp || tractce as census_tract
- , year_
+ select *
from {{ ref('census_tracts') }}
where
year_ <= 2020
and census_tract_id in (select census_tract_id from in_city_boundary)
)
+, white as (
+ select * from {{ ref('acs_tract') }}
+ where name_ = 'B02001_002E' -- white population
+)
+, income as (
+ select * from {{ ref('acs_tract') }}
+ where name_ = 'B19013_001E' -- median household income
+)
, raw_data as (
select
census_tracts.census_tract
@@ -32,6 +37,8 @@ select
, distance_to_transit.mean_distance_to_transit
, parcel_area.parcel_sqm
, parking_limits.mean_limit
+ , white.value_ as white
+ , income.value_ as income
from
census_tracts
inner join housing_units using (census_tract_id)
@@ -39,6 +46,8 @@ from
inner join distance_to_transit using (census_tract_id)
inner join parcel_area using (census_tract_id)
inner join parking_limits using (census_tract_id)
+ inner join white using (census_tract_id)
+ inner join income using (census_tract_id)
)
, with_std as (
select
@@ -50,7 +59,9 @@ select
, median_distance_to_transit
, mean_distance_to_transit
, parcel_sqm
- , {{ standardize(['num_units', 'total_value', 'median_value', 'median_distance_to_transit', 'mean_distance_to_transit', 'parcel_sqm']) }}
+ , {{ standardize(['num_units', 'total_value', 'median_value',
+ 'median_distance_to_transit', 'mean_distance_to_transit',
+ 'parcel_sqm', 'white', 'income' ]) }}
from
raw_data
)
From 3ab57e7cab165d249fb387e4ff0362297c6dc0dc Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 23 Aug 2024 15:26:17 -0400
Subject: [PATCH 084/142] readd field name
---
dbt/models/parcels_base.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 4e7a6fbb..4929f82b 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -26,7 +26,7 @@ parcels_union as (
nullif(emv_bldg, 0) as emv_bldg,
nullif(emv_total, 0) as emv_total,
nullif(year_built, 0) as year_built,
- nullif(sale_date, '1899-12-30'::date),
+ nullif(sale_date, '1899-12-30'::date) as sale_date,
nullif(sale_value, 0) as sale_value,
st_transform(geom, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'parcels_shp_plan_regonal_' ~ year_ ~ '_parcels' ~ year_ ~ 'hennepin') }}
From 353bcfeec501c079715381338b446ed9a444192b Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 23 Aug 2024 15:27:19 -0400
Subject: [PATCH 085/142] add whiteness and income demographic variables to
census_tracts_wide
---
dbt/models/acs_tract.sql | 9 +++----
dbt/models/census_tracts.sql | 37 ++++++++++++++++++++++++----
dbt/models/census_tracts_wide.sql | 40 ++++++++++++++++++++++++++-----
dbt/models/schema.yml | 15 ------------
4 files changed, 70 insertions(+), 31 deletions(-)
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index 3909b2dc..96fc0a02 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -2,7 +2,7 @@
config(
materialized='table',
indexes = [
- {'columns': ['census_tract_id', 'year_', 'name_'], 'unique': true},
+ {'columns': ['census_tract', 'year_', 'name_'], 'unique': true},
]
)
}}
@@ -11,13 +11,10 @@ with
census_tracts as (select * from {{ ref("census_tracts") }})
, acs_tract as (select * from {{ ref('acs_tract_clean') }})
select
- census_tract_id
+ census_tract
, acs_tract.year_
, acs_tract.name_
, acs_tract.value_
from
acs_tract
- inner join census_tracts
- using (statefp, countyfp, tractce)
- where
- to_date(acs_tract.year_::text , 'YYYY') <@ census_tracts.valid
+ inner join census_tracts using (statefp, countyfp, tractce, year_)
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 4ffe0004..28ff79e6 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -16,14 +16,13 @@ select
, county as countyfp
, tract as tractce
, geo_id as geoidfq
- , '[,2013-01-01)'::daterange as valid
{% else %}
statefp
, countyfp
, tractce
, {{ 'geoidfq' if year_ >= 2023 else 'affgeoid' }} as geoidfq
- , '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
{% endif %}
+ , '[{{year_}}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid
, {{ year_ }} as year_
, st_transform(geom, {{ var("srid") }}) as geom
from
@@ -31,11 +30,41 @@ from
{% if not loop.last %}union all{% endif %}
{% endfor %}
),
+years_2011_2012 as (
+ select
+ statefp
+ , countyfp
+ , tractce
+ , geoidfq
+ , '[2011-01-01,2012-01-01)'::daterange as valid
+ , 2011 as year_
+ , geom
+ from census_tracts_union
+ where year_ = 2010
+ union all
+ select
+ statefp
+ , countyfp
+ , tractce
+ , geoidfq
+ , '[2012-01-01,2013-01-01)'::daterange as valid
+ , 2012 as year_
+ , geom
+ from census_tracts_union
+ where year_ = 2010
+),
+add_2011_2012 as (
+ select *
+ from census_tracts_union
+ union all
+ select *
+ from years_2011_2012
+),
with_census_tract as (
select *, statefp || countyfp || tractce as census_tract
- from census_tracts_union
+ from add_2011_2012
)
select
- {{ dbt_utils.generate_surrogate_key(['geoidfq', 'valid']) }} as census_tract_id, *
+ {{ dbt_utils.generate_surrogate_key(['geoidfq', 'year_']) }} as census_tract_id, *
from
with_census_tract
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index e078af39..677aff99 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -11,6 +11,7 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
, distance_to_transit as (select * from {{ ref('census_tracts_distance_to_transit') }})
, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
+, acs_tract as (select * from {{ ref('acs_tract') }})
, census_tracts as (
select *
from {{ ref('census_tracts') }}
@@ -18,12 +19,37 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
year_ <= 2020
and census_tract_id in (select census_tract_id from in_city_boundary)
)
+
+-- Fill in data for 2011, 2012 using closest available year. Replace 2020 data
+-- with 2019 data to avoid pandemic effects.
+, acs_replace_years as (
+ select * from acs_tract where year_ != 2020
+ union all
+ select census_tract, 2020 as year_, name_, value_
+ from acs_tract where year_ = 2019
+ union all
+ -- select * from acs_tract
+ -- union all
+ select census_tract, 2011 as year_, name_, value_
+ from acs_tract where year_ = 2013
+ union all
+ select census_tract, 2012 as year_, name_, value_
+ from acs_tract where year_ = 2013
+)
, white as (
- select * from {{ ref('acs_tract') }}
- where name_ = 'B02001_002E' -- white population
+ select * from acs_replace_years
+ where name_ = 'B03002_003E' -- white non-hispanic population
+)
+, population as (
+ select * from acs_replace_years
+ where name_ = 'B01003_001E' -- total population
+)
+, white_frac as (
+ select white.census_tract, white.year_, {{ safe_divide('white.value_', 'population.value_') }} as value_
+ from white inner join population using (census_tract, year_)
)
, income as (
- select * from {{ ref('acs_tract') }}
+ select * from acs_replace_years
where name_ = 'B19013_001E' -- median household income
)
, raw_data as (
@@ -37,7 +63,7 @@ select
, distance_to_transit.mean_distance_to_transit
, parcel_area.parcel_sqm
, parking_limits.mean_limit
- , white.value_ as white
+ , white_frac.value_ as white
, income.value_ as income
from
census_tracts
@@ -46,8 +72,8 @@ from
inner join distance_to_transit using (census_tract_id)
inner join parcel_area using (census_tract_id)
inner join parking_limits using (census_tract_id)
- inner join white using (census_tract_id)
- inner join income using (census_tract_id)
+ left join white_frac using (census_tract, year_)
+ left join income using (census_tract, year_)
)
, with_std as (
select
@@ -59,6 +85,8 @@ select
, median_distance_to_transit
, mean_distance_to_transit
, parcel_sqm
+ , white
+ , income
, {{ standardize(['num_units', 'total_value', 'median_value',
'median_distance_to_transit', 'mean_distance_to_transit',
'parcel_sqm', 'white', 'income' ]) }}
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index f9b0ddab..b6d23411 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -108,20 +108,6 @@ models:
to: ref('census_tracts')
field: census_tract_id
- - name: acs_tract
- data_tests:
- - dbt_utils.unique_combination_of_columns:
- combination_of_columns:
- - census_tract_id
- - year_
- - name_
- columns:
- - name: census_tract_id
- data_tests:
- - relationships:
- to: ref('census_tracts')
- field: census_tract_id
-
- name: acs_block_group
data_tests:
- dbt_utils.unique_combination_of_columns:
@@ -164,7 +150,6 @@ models:
field: zip_code_id
- name: census_block_group_id
data_tests:
- - not_null
- relationships:
to: ref('census_block_groups')
field: census_block_group_id
From a5b311fae391d1168689104a01d457f71f0dec3f Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 23 Aug 2024 16:06:47 -0400
Subject: [PATCH 086/142] correctly handle the downtown parking limit
elimination
---
dbt/models/census_tracts_parking_limits.sql | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/dbt/models/census_tracts_parking_limits.sql b/dbt/models/census_tracts_parking_limits.sql
index b40f89ea..be6a25f1 100644
--- a/dbt/models/census_tracts_parking_limits.sql
+++ b/dbt/models/census_tracts_parking_limits.sql
@@ -10,11 +10,13 @@
with
parcels as (select * from {{ ref('parcels') }}),
transit as (select * from {{ ref('high_frequency_transit_lines') }}),
+downtown as (select * from {{ ref('downtown') }}),
with_parking_limit as (
select
parcel_id,
census_tract_id,
case
+ when st_intersects(parcels.geom, downtown.geom) then 'eliminated'
when parcels.valid << '[2015-01-01,)'::daterange then 'full'
else
case
@@ -24,7 +26,7 @@ with_parking_limit as (
end
end as limit_
from
- parcels
+ downtown, parcels
left join transit
on parcels.valid && transit.valid
),
From ea007ac8159bddc234474cd972f8ce67f33d841f Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 23 Aug 2024 16:06:59 -0400
Subject: [PATCH 087/142] add mean_limit to the wide table
---
dbt/models/census_tracts_wide.sql | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index 677aff99..24333eb2 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -87,9 +87,10 @@ select
, parcel_sqm
, white
, income
+ , mean_limit
, {{ standardize(['num_units', 'total_value', 'median_value',
'median_distance_to_transit', 'mean_distance_to_transit',
- 'parcel_sqm', 'white', 'income' ]) }}
+ 'parcel_sqm', 'white', 'income', 'mean_limit' ]) }}
from
raw_data
)
From 95973f636bf92640ef1671b9ce3c6f59d1b1caff Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 26 Aug 2024 11:17:40 -0400
Subject: [PATCH 088/142] correctly load hispanic_or_latino acs data
---
dbt/models/schema.yml | 11 ++++++++++
dbt/seeds/acs_variables.csv | 2 +-
dbt/seeds/population_categories.csv | 2 +-
load_data_server/load_acs.py | 33 ++++++++++++++++-------------
4 files changed, 31 insertions(+), 17 deletions(-)
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index b6d23411..2b6698eb 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -295,3 +295,14 @@ models:
data_tests:
- not_null
- unique
+
+seeds:
+ - name: population_categories
+ columns:
+ - name: category
+ data_tests:
+ - unique
+ - not_null
+ - relationships:
+ to: ref('acs_variables')
+ field: description
diff --git a/dbt/seeds/acs_variables.csv b/dbt/seeds/acs_variables.csv
index 8cdeba7c..5520ef20 100644
--- a/dbt/seeds/acs_variables.csv
+++ b/dbt/seeds/acs_variables.csv
@@ -20,7 +20,7 @@ B02001_003E,black
B02001_004E,american_indian_or_alaska_native
B02001_005E,asian
B02001_006E,native_hawaiian_or_pacific_islander
-B03001_003E,population_hispanic_or_latino
+B03001_003E,hispanic_or_latino
B02001_007E,other_race
B02001_008E,multiple_races
B02001_009E,multiple_races_and_other_race
diff --git a/dbt/seeds/population_categories.csv b/dbt/seeds/population_categories.csv
index 79e93b14..501dbf73 100644
--- a/dbt/seeds/population_categories.csv
+++ b/dbt/seeds/population_categories.csv
@@ -1,7 +1,7 @@
category
population_white_non_hispanic
population_black_non_hispanic
-population_hispanic_or_latino
+hispanic_or_latino
population_asian_non_hispanic
population_native_hawaiian_or_pacific_islander_non_hispanic
population_american_indian_or_alaska_native_non_hispanic
diff --git a/load_data_server/load_acs.py b/load_data_server/load_acs.py
index 23ae704e..9ace57b6 100644
--- a/load_data_server/load_acs.py
+++ b/load_data_server/load_acs.py
@@ -49,7 +49,7 @@
"B02001_004E": "american_indian_or_alaska_native",
"B02001_005E": "asian",
"B02001_006E": "native_hawaiian_or_pacific_islander",
- "B03001_003E": "population_hispanic_or_latino",
+ "B03001_003E": "hispanic_or_latino",
"B02001_007E": "other_race",
"B02001_008E": "multiple_races",
"B02001_009E": "multiple_races_and_other_race",
@@ -78,14 +78,6 @@
"B02015_023E": "south_asian_bhutanese",
"B02015_024E": "south_asian_nepalese",
"B02015_025E": "south_asian_pakistani",
- "B02015_026E": "south_asian_sikh",
- "B02015_027E": "south_asian_sri_lankan",
- "B02015_028E": "south_asian_other",
- "B02015_029E": "central_asian_kazakh",
- "B02015_030E": "central_asian_uzbek",
- "B02015_031E": "central_asian_other",
- "B02015_032E": "other_asian_specified",
- "B02015_033E": "other_asian_not_specified",
"B19013_001E": "median_household_income",
"B19013A_001E": "median_household_income_white",
"B19013H_001E": "median_household_income_white_non_hispanic",
@@ -128,10 +120,10 @@
bucket = storage_client.bucket(BUCKET_NAME)
cur = conn.cursor()
- cur.execute(f"drop table if exists {SCHEMA}.acs_tract_raw")
cur.execute(
- f"create table {SCHEMA}.acs_tract_raw (statefp text, countyfp text, tractce text, year int, code text, value numeric)"
+ f"create table if not exists {SCHEMA}.acs_tract_raw (statefp text, countyfp text, tractce text, year int, code text, value numeric)"
)
+ cur.execute(f"truncate table {SCHEMA}.acs_tract_raw")
temp_table = f"{SCHEMA}.acs_tract_temp"
cur.execute(f"drop table if exists {temp_table}")
@@ -141,7 +133,12 @@
for code in tqdm(ACS_CODES.keys()):
desc = ACS_CODES[code]
- for blob in bucket.list_blobs(prefix=f"acs/tracts/{desc}/"):
+ blobs = list(bucket.list_blobs(prefix=f"acs/tracts/{desc}/"))
+ if len(blobs) == 0:
+ logging.info(f"No blobs found for {desc}")
+ continue
+
+ for blob in blobs:
year = blob.name.split("/")[-1].split(".")[0]
cur.execute(f"truncate {temp_table}")
with tempfile.NamedTemporaryFile() as temp:
@@ -155,10 +152,10 @@
cur.execute(f"drop table {temp_table}")
conn.commit()
- cur.execute(f"drop table if exists {SCHEMA}.acs_bg_raw")
cur.execute(
- f"create table {SCHEMA}.acs_bg_raw (statefp text, countyfp text, tractce text, blkgrpce text, year int, code text, value numeric)"
+ f"create table if not exists {SCHEMA}.acs_bg_raw (statefp text, countyfp text, tractce text, blkgrpce text, year int, code text, value numeric)"
)
+ cur.execute(f"truncate table {SCHEMA}.acs_bg_raw")
temp_table = f"{SCHEMA}.acs_tract_temp"
cur.execute(f"drop table if exists {temp_table}")
@@ -168,7 +165,13 @@
for code in tqdm(ACS_CODES.keys()):
desc = ACS_CODES[code]
- for blob in bucket.list_blobs(prefix=f"acs/block_groups/{desc}/"):
+
+ blobs = list(bucket.list_blobs(prefix=f"acs/block_groups/{desc}/"))
+ if len(blobs) == 0:
+ logging.info(f"No blobs found for {desc}")
+ continue
+
+ for blob in blobs:
year = blob.name.split("/")[-1].split(".")[0]
cur.execute(f"truncate {temp_table}")
with tempfile.NamedTemporaryFile() as temp:
From 860565241dd236e992c9c3f4bcc135faf070eae5 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 26 Aug 2024 11:18:19 -0400
Subject: [PATCH 089/142] filter census tracts to city boundary when computing
segregation indexes
---
dbt/models/census_tracts_in_city_boundary.sql | 2 +
dbt/models/segregation_indexes.sql | 64 +++++++++++--------
2 files changed, 38 insertions(+), 28 deletions(-)
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
index be4771e3..b19de633 100644
--- a/dbt/models/census_tracts_in_city_boundary.sql
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -6,6 +6,8 @@ with census_tracts as (
)
select
census_tracts.census_tract_id
+ , census_tracts.census_tract
+ , census_tracts.year_
from
census_tracts
, city_boundary
diff --git a/dbt/models/segregation_indexes.sql b/dbt/models/segregation_indexes.sql
index ea47ba0f..4722ecef 100644
--- a/dbt/models/segregation_indexes.sql
+++ b/dbt/models/segregation_indexes.sql
@@ -1,3 +1,12 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['census_tract', 'year_', 'distribution'], 'unique': true},
+ ]
+ )
+}}
+
-- Segregation index for each tract for each year, computed for each reference
-- distribution.
--
@@ -7,37 +16,41 @@
-- segregation index for the 'average_city' distribution.
with
categories as (select * from {{ ref("population_categories") }})
- , acs_tract as (select * from {{ ref("acs_tract") }})
+ , acs_tract_all as (select * from {{ ref("acs_tract") }})
, acs_variables as (
select
variable as name_,
description
from {{ ref("acs_variables") }}
)
+ , census_tracts_in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
+ , acs_tract as (
+ select * from acs_tract_all inner join census_tracts_in_city_boundary using (census_tract, year_)
+ )
, pop_tyc as
( -- Population by tract, year, and category
- select acs_tract.census_tract_id, acs_tract.year_, categories.category, acs_tract.value_
+ select acs_tract.census_tract, acs_tract.year_, categories.category, acs_tract.value_
from acs_tract
- join acs_variables using (name_)
- join categories on categories.category = acs_variables.description
+ inner join acs_variables using (name_)
+ inner join categories on categories.category = acs_variables.description
),
pop_ty as
- ( -- Population by tract and year (note: using 'population' variable instead of aggregating categories)
- select census_tract_id, year_, value_
- from acs_tract join acs_variables using (name_)
- where acs_variables.description = 'population'
+ (
+ select census_tract, year_, sum(value_) as value_
+ from pop_tyc
+ group by 1, 2
),
pop_yc as
( -- Population by year and category
select year_, category, sum(value_) as value_
from pop_tyc
- group by year_, category
+ group by 1, 2
),
pop_y as
( -- Population by year
select year_, sum(value_) as value_
- from pop_ty
- group by year_
+ from pop_tyc
+ group by 1
),
dist_yc as
( -- Distribution of population by year and category
@@ -45,18 +58,16 @@ with
pop_yc.year_,
pop_yc.category,
{{ safe_divide('pop_yc.value_', 'pop_y.value_') }} as value_
- from pop_yc
- inner join pop_y using (year_)
+ from pop_yc inner join pop_y using (year_)
),
dist_tyc as
( -- Distribution of population by tract, year, and category
select
- pop_tyc.census_tract_id,
+ pop_tyc.census_tract,
pop_tyc.year_,
pop_tyc.category,
{{ safe_divide('pop_tyc.value_', 'pop_ty.value_') }} as value_
- from pop_tyc
- inner join pop_ty using (year_, census_tract_id)
+ from pop_tyc inner join pop_ty using (year_, census_tract)
),
uniform_dist as
( -- Uniform distribution across categories
@@ -68,40 +79,37 @@ with
( -- Average of the annual citywide distributions
select category, avg(value_) as value_
from dist_yc
- group by category
+ group by 1
)
select
- census_tract_id,
+ census_tract,
year_,
dist as distribution,
sum(case when p = 0 or q = 0 then 0 else p * ln(p / q) end) as segregation_index
from
(
select
- dist_tyc.census_tract_id,
+ dist_tyc.census_tract,
dist_tyc.year_,
dist_tyc.value_ as p,
uniform_dist.value_ as q,
'uniform' as dist
- from dist_tyc
- inner join uniform_dist using (category)
+ from dist_tyc inner join uniform_dist using (category)
union all
select
- dist_tyc.census_tract_id,
+ dist_tyc.census_tract,
dist_tyc.year_,
dist_tyc.value_ as p,
dist_yc.value_ as q,
'annual_city' as dist
- from dist_tyc
- inner join dist_yc using (year_, category)
+ from dist_tyc inner join dist_yc using (year_, category)
union all
select
- dist_tyc.census_tract_id,
+ dist_tyc.census_tract,
dist_tyc.year_,
dist_tyc.value_ as p,
average_dist.value_ as q,
'average_city' as dist
- from dist_tyc
- inner join average_dist using (category)
+ from dist_tyc inner join average_dist using (category)
)
-group by census_tract_id, year_, dist
+group by 1, 2, 3
From f69a868bb110656894913d3b58a71e0ebe3d817a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Mon, 26 Aug 2024 13:12:55 -0400
Subject: [PATCH 090/142] unify approach to year replacement for demographic
data
---
dbt/models/census_tracts_wide.sql | 41 ++++++++++++++++++++++---------
dbt/models/schema.yml | 6 ++---
2 files changed, 32 insertions(+), 15 deletions(-)
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index 24333eb2..f92d7a9a 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -11,7 +11,6 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
, distance_to_transit as (select * from {{ ref('census_tracts_distance_to_transit') }})
, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
-, acs_tract as (select * from {{ ref('acs_tract') }})
, census_tracts as (
select *
from {{ ref('census_tracts') }}
@@ -20,28 +19,38 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
and census_tract_id in (select census_tract_id from in_city_boundary)
)
+-- Demographic data
+, acs_tract as (select * from {{ ref('acs_tract') }})
+, segregation_indexes as (
+ select census_tract, year_, 'segregation', segregation_index as value_
+ from {{ ref('segregation_indexes') }}
+ where distribution = 'annual_city'
+)
+, demographics as (
+ select * from acs_tract
+ union all
+ select * from segregation_indexes
+)
-- Fill in data for 2011, 2012 using closest available year. Replace 2020 data
-- with 2019 data to avoid pandemic effects.
-, acs_replace_years as (
- select * from acs_tract where year_ != 2020
+, demographics_replace_years as (
+ select * from demographics where year_ != 2020
union all
select census_tract, 2020 as year_, name_, value_
- from acs_tract where year_ = 2019
+ from demographics where year_ = 2019
union all
- -- select * from acs_tract
- -- union all
select census_tract, 2011 as year_, name_, value_
- from acs_tract where year_ = 2013
+ from demographics where year_ = 2013
union all
select census_tract, 2012 as year_, name_, value_
- from acs_tract where year_ = 2013
+ from demographics where year_ = 2013
)
, white as (
- select * from acs_replace_years
+ select * from demographics_replace_years
where name_ = 'B03002_003E' -- white non-hispanic population
)
, population as (
- select * from acs_replace_years
+ select * from demographics_replace_years
where name_ = 'B01003_001E' -- total population
)
, white_frac as (
@@ -49,9 +58,14 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
from white inner join population using (census_tract, year_)
)
, income as (
- select * from acs_replace_years
+ select * from demographics_replace_years
where name_ = 'B19013_001E' -- median household income
)
+, segregation as (
+ select * from demographics_replace_years
+ where name_ = 'segregation'
+)
+
, raw_data as (
select
census_tracts.census_tract
@@ -65,6 +79,7 @@ select
, parking_limits.mean_limit
, white_frac.value_ as white
, income.value_ as income
+ , segregation.value_ as segregation
from
census_tracts
inner join housing_units using (census_tract_id)
@@ -72,6 +87,7 @@ from
inner join distance_to_transit using (census_tract_id)
inner join parcel_area using (census_tract_id)
inner join parking_limits using (census_tract_id)
+ inner join segregation using (census_tract, year_)
left join white_frac using (census_tract, year_)
left join income using (census_tract, year_)
)
@@ -88,9 +104,10 @@ select
, white
, income
, mean_limit
+ , segregation
, {{ standardize(['num_units', 'total_value', 'median_value',
'median_distance_to_transit', 'mean_distance_to_transit',
- 'parcel_sqm', 'white', 'income', 'mean_limit' ]) }}
+ 'parcel_sqm', 'white', 'income', 'mean_limit', 'segregation' ]) }}
from
raw_data
)
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 2b6698eb..2ed844c8 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -126,15 +126,15 @@ models:
data_tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- - census_tract_id
+ - census_tract
- year_
- distribution
columns:
- - name: census_tract_id
+ - name: census_tract
data_tests:
- relationships:
to: ref('census_tracts')
- field: census_tract_id
+ field: census_tract
- name: parcels
columns:
From b77631093db8e47236c0a0cf67e61b906dc28f9d Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 27 Aug 2024 13:55:28 -0400
Subject: [PATCH 091/142] fix up acs_tract_wide
---
dbt/models/acs_tract_wide.sql | 50 +++++++++++------------------------
1 file changed, 16 insertions(+), 34 deletions(-)
diff --git a/dbt/models/acs_tract_wide.sql b/dbt/models/acs_tract_wide.sql
index 0d142795..543c38e7 100644
--- a/dbt/models/acs_tract_wide.sql
+++ b/dbt/models/acs_tract_wide.sql
@@ -11,53 +11,35 @@
with
acs_tract as (select * from {{ ref('acs_tract') }})
-, acs_variables as (select * from {{ ref("acs_variables") }})
-, census_tracts_in_city_boundary as (
- select * from {{ ref("census_tracts_in_city_boundary") }}
+, acs_variables as (select * from {{ ref('acs_variables') }})
+, census_tracts as (select * from {{ ref('census_tracts_in_city_boundary') }})
+, acs_tract_filtered as (
+ select acs_tract.*, description
+ from acs_tract
+ inner join census_tracts using (census_tract, year_)
+ inner join acs_variables on acs_tract.name_ = acs_variables.variable
)
-, census_tracts as (
- select
- census_tract_id
- , substring(geoidfq from 10) as geoidfq
- from {{ ref("census_tracts") }}
- where census_tract_id in (select census_tract_id from census_tracts_in_city_boundary)
-)
-, acs_tract_extended as (
- select
- acs_tract.census_tract_id
- , census_tracts.geoidfq
- , acs_tract.year_
- , acs_tract.name_
- , acs_tract.value_
- from
- acs_tract
- inner join census_tracts using (census_tract_id)
-)
-
, distinct_tracts_and_variables as (
select distinct
- geoidfq
+ census_tract
, name_
- from acs_tract_extended
+ , description
+ from acs_tract_filtered
)
-
select
- acs_variables.description
- , distinct_tracts_and_variables.geoidfq as tract_id
+ description
+ , census_tract as tract_id
{% for year_ in years %}
, "{{ year_ }}"
{% endfor %}
-from
-distinct_tracts_and_variables
-inner join acs_variables
- on distinct_tracts_and_variables.name_ = acs_variables.variable
+from distinct_tracts_and_variables
{% for year_ in years %}
left join
(select
- geoidfq
+ census_tract
, name_
, value_ as "{{ year_}}"
-from acs_tract_extended
+from acs_tract_filtered
where year_ = {{ year_ }})
-using (geoidfq, name_)
+using (census_tract, name_)
{% endfor %}
From 465bbfe1267558dcb46ec5eb0bcc436928a298d6 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 27 Aug 2024 15:20:42 -0400
Subject: [PATCH 092/142] add type casts
---
dbt/macros/standardize.sql | 2 +-
dbt/models/census_tracts_housing_units.sql | 2 +-
dbt/models/commercial_permits.sql | 20 ++++++++--------
dbt/models/fair_market_rents.sql | 4 ++--
dbt/models/parcels_base.sql | 10 ++++----
dbt/models/parking.sql | 14 +++++------
dbt/models/residential_permits.sql | 28 +++++++++++-----------
dbt/models/segregation_indexes.sql | 8 +++----
dbt/models/usps_migration.sql | 4 ++--
9 files changed, 46 insertions(+), 46 deletions(-)
diff --git a/dbt/macros/standardize.sql b/dbt/macros/standardize.sql
index 795ebad2..63ec955e 100644
--- a/dbt/macros/standardize.sql
+++ b/dbt/macros/standardize.sql
@@ -1,6 +1,6 @@
{% macro standardize(columns) %}
{% for c in columns %}
- (({{ c }} - (avg({{ c }}) over ())) / (stddev_samp({{ c }}) over ())) as std_{{ c }}
+ (({{ c }} - (avg({{ c }}) over ())) / (stddev_samp({{ c }}) over ()))::double precision as std_{{ c }}
{% if not loop.last %},{% endif %}
{% endfor %}
{% endmacro %}
diff --git a/dbt/models/census_tracts_housing_units.sql b/dbt/models/census_tracts_housing_units.sql
index c60779e2..38c91359 100644
--- a/dbt/models/census_tracts_housing_units.sql
+++ b/dbt/models/census_tracts_housing_units.sql
@@ -18,7 +18,7 @@ with census_tracts as (
)
select
census_tracts.census_tract_id
- , sum(residential_permits.num_units) as num_units
+ , sum(residential_permits.num_units)::int as num_units
from
census_tracts
left join residential_permits_to_census_tracts using (census_tract_id)
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index b51cb23d..4687eb30 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -11,17 +11,17 @@
select
sde_id as commercial_permit_id
, year::int as year_
- , nonres_gro as group_
- , nonres_sub as subgroup
- , nonres_typ as type_category
- , bldg_name as building_name
- , bldg_desc as building_description
- , permit_typ as permit_type
- , permit_val as permit_value
- , sqf as square_feet
- , address
+ , nonres_gro::text as group_
+ , nonres_sub::text as subgroup
+ , nonres_typ::text as type_category
+ , bldg_name::text as building_name
+ , bldg_desc::text as building_description
+ , permit_typ::text as permit_type
+ , permit_val::int as permit_value
+ , nullif(sqf, 0)::int as square_feet
+ , address::text
, st_transform(geom, {{ var("srid") }}) as geom
- from
+from
{{ source('minneapolis', 'commercial_permits_nonresidentialconstruction') }}
where
co_code = '053'
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index a9a9cdbc..847d19e1 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -19,9 +19,9 @@ zip_codes as (select * from {{ ref('zip_codes') }})
{% for bedroom in num_bedrooms %}
select
zip_code_id
- , rent_br{{ bedroom }} as rent
+ , rent_br{{ bedroom }}::int as rent
, {{ bedroom }} as num_bedrooms
- , year_
+ , year_::int
from fmr_zip
{% if not loop.last %} union all {% endif %}
{% endfor %}
diff --git a/dbt/models/parcels_base.sql b/dbt/models/parcels_base.sql
index 4929f82b..d4b0a7c9 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/parcels_base.sql
@@ -22,12 +22,12 @@ parcels_union as (
-- parcels are a year-end snapshot, named after the year they cover
'[{{ year_ }}-01-01,{{ year_ + 1 }}-01-01)'::daterange as valid,
- nullif(emv_land, 0) as emv_land,
- nullif(emv_bldg, 0) as emv_bldg,
- nullif(emv_total, 0) as emv_total,
- nullif(year_built, 0) as year_built,
+ nullif(emv_land, 0)::int as emv_land,
+ nullif(emv_bldg, 0)::int as emv_bldg,
+ nullif(emv_total, 0)::int as emv_total,
+ nullif(year_built, 0)::int as year_built,
nullif(sale_date, '1899-12-30'::date) as sale_date,
- nullif(sale_value, 0) as sale_value,
+ nullif(sale_value, 0)::int as sale_value,
st_transform(geom, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'parcels_shp_plan_regonal_' ~ year_ ~ '_parcels' ~ year_ ~ 'hennepin') }}
where upper({{ "city" if year_ < 2018 else "ctu_name" }}) = '{{ city }}'
diff --git a/dbt/models/parking.sql b/dbt/models/parking.sql
index cd0b874e..ac31de4a 100644
--- a/dbt/models/parking.sql
+++ b/dbt/models/parking.sql
@@ -18,13 +18,13 @@ with
select
ogc_fid as parking_id
, to_date("year" || '-' || "date", 'YYYY-DD-Mon') as date_
- , "project na" as project_name
- , address
- , neighborho as neighborhood
- , ward
+ , "project na"::text as project_name
+ , address::text
+ , neighborho::text as neighborhood
+ , ward::int
, "downtown y" = 'Y' as is_downtown
- , "housing un" as num_housing_units
- , "car parkin" as num_car_parking_spaces
- , "bike parki" as num_bike_parking_spaces
+ , "housing un"::int as num_housing_units
+ , "car parkin"::int as num_car_parking_spaces
+ , "bike parki"::int as num_bike_parking_spaces
, st_transform(geom, {{ var("srid") }}) as geom
from parking_raw
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 35018922..6f994a0a 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -11,22 +11,22 @@
select
sde_id::int as residential_permit_id
, year::int as year_
- , tenure
- , housing_ty as housing_type
- , res_permit as permit_type
- , address
- , name as name_
- , buildings as num_buildings
- , units as num_units
- , age_restri as num_age_restricted_units
- , memory_car as num_memory_care_units
- , assisted as num_assisted_living_units
+ , tenure::text
+ , housing_ty::text as housing_type
+ , res_permit::text as permit_type
+ , address::text
+ , name::text as name_
+ , buildings::int as num_buildings
+ , units::int as num_units
+ , age_restri::int as num_age_restricted_units
+ , memory_car::int as num_memory_care_units
+ , assisted::int as num_assisted_living_units
, com_off_re = 'Y' as is_commercial_and_residential
- , nullif(sqf, 0) as square_feet
+ , nullif(sqf, 0)::int as square_feet
, public_fun = 'Y' as is_public_funded
- , nullif(permit_val, 0) as permit_value
- , community_ as community_designation
- , notes
+ , nullif(permit_val, 0)::int as permit_value
+ , community_::text as community_designation
+ , notes::text
, st_transform(geom, {{ var("srid") }}) as geom
from
{{ source('minneapolis', 'residential_permits_residentialpermits') }}
diff --git a/dbt/models/segregation_indexes.sql b/dbt/models/segregation_indexes.sql
index 4722ecef..90f48b7b 100644
--- a/dbt/models/segregation_indexes.sql
+++ b/dbt/models/segregation_indexes.sql
@@ -57,7 +57,7 @@ with
select
pop_yc.year_,
pop_yc.category,
- {{ safe_divide('pop_yc.value_', 'pop_y.value_') }} as value_
+ ({{ safe_divide('pop_yc.value_', 'pop_y.value_') }})::double precision as value_
from pop_yc inner join pop_y using (year_)
),
dist_tyc as
@@ -66,18 +66,18 @@ with
pop_tyc.census_tract,
pop_tyc.year_,
pop_tyc.category,
- {{ safe_divide('pop_tyc.value_', 'pop_ty.value_') }} as value_
+ ({{ safe_divide('pop_tyc.value_', 'pop_ty.value_') }})::double precision as value_
from pop_tyc inner join pop_ty using (year_, census_tract)
),
uniform_dist as
( -- Uniform distribution across categories
with n_cat as (select count(*) as n_cat from categories)
- select category, 1.0 / n_cat as value_
+ select category, (1.0 / n_cat)::double precision as value_
from categories, n_cat
),
average_dist as
( -- Average of the annual citywide distributions
- select category, avg(value_) as value_
+ select category, avg(value_)::double precision as value_
from dist_yc
group by 1
)
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 2cafef6c..541b32c1 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -30,7 +30,7 @@ zip_codes as (select * from {{ ref('zip_codes') }})
, zip_code_id
, '{{ flow_direction }}' as flow_direction
, 'total' as flow_type
- , total_{{ flow_direction }}_zip as flow_value
+ , total_{{ flow_direction }}_zip::int as flow_value
from add_zip_id
union all
{% for flow_type in usps_migration_flow_types %}
@@ -39,7 +39,7 @@ zip_codes as (select * from {{ ref('zip_codes') }})
, zip_code_id
, '{{ flow_direction }}' as flow_direction
, '{{ flow_type }}' as flow_type
- , total_{{ flow_direction }}_zip_{{ flow_type }} as flow_value
+ , total_{{ flow_direction }}_zip_{{ flow_type }}::int as flow_value
from add_zip_id
{% if not loop.last %} union all {% endif %}
{% endfor %}
From be166d267e08dd93c220cfb5ab9b0512ed37a191 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 27 Aug 2024 16:05:03 -0400
Subject: [PATCH 093/142] extract demographics table
---
dbt/models/census_tracts_wide.sql | 36 +++++--------------------
dbt/models/demographics.sql | 45 +++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 30 deletions(-)
create mode 100644 dbt/models/demographics.sql
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index f92d7a9a..05bede3e 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -11,6 +11,7 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
, distance_to_transit as (select * from {{ ref('census_tracts_distance_to_transit') }})
, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
+, demographics as (select * from {{ ref('demographics') }})
, census_tracts as (
select *
from {{ ref('census_tracts') }}
@@ -20,37 +21,12 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
)
-- Demographic data
-, acs_tract as (select * from {{ ref('acs_tract') }})
-, segregation_indexes as (
- select census_tract, year_, 'segregation', segregation_index as value_
- from {{ ref('segregation_indexes') }}
- where distribution = 'annual_city'
-)
-, demographics as (
- select * from acs_tract
- union all
- select * from segregation_indexes
-)
--- Fill in data for 2011, 2012 using closest available year. Replace 2020 data
--- with 2019 data to avoid pandemic effects.
-, demographics_replace_years as (
- select * from demographics where year_ != 2020
- union all
- select census_tract, 2020 as year_, name_, value_
- from demographics where year_ = 2019
- union all
- select census_tract, 2011 as year_, name_, value_
- from demographics where year_ = 2013
- union all
- select census_tract, 2012 as year_, name_, value_
- from demographics where year_ = 2013
-)
, white as (
- select * from demographics_replace_years
+ select * from demographics
where name_ = 'B03002_003E' -- white non-hispanic population
)
, population as (
- select * from demographics_replace_years
+ select * from demographics
where name_ = 'B01003_001E' -- total population
)
, white_frac as (
@@ -58,12 +34,12 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
from white inner join population using (census_tract, year_)
)
, income as (
- select * from demographics_replace_years
+ select * from demographics
where name_ = 'B19013_001E' -- median household income
)
, segregation as (
- select * from demographics_replace_years
- where name_ = 'segregation'
+ select * from demographics
+ where description = 'segregation_index_annual_city'
)
, raw_data as (
diff --git a/dbt/models/demographics.sql b/dbt/models/demographics.sql
new file mode 100644
index 00000000..3720dac5
--- /dev/null
+++ b/dbt/models/demographics.sql
@@ -0,0 +1,45 @@
+-- Demographic data
+-- Contains data from the ACS and the computed segregation indexes.
+with
+acs_tract as (select * from {{ ref('acs_tract') }}),
+acs_variables as (select * from {{ ref('acs_variables') }}),
+acs_tract_with_description as (
+ select
+ acs_tract.census_tract,
+ acs_tract.year_,
+ acs_tract.name_,
+ acs_variables.description,
+ acs_tract.value_
+ from acs_tract
+ inner join acs_variables on acs_tract.name_ = acs_variables.variable
+),
+segregation_indexes as (
+ select
+ census_tract,
+ year_,
+ null as name_,
+ 'segregation_index_' || distribution as description,
+ segregation_index as value_
+ from {{ ref('segregation_indexes') }}
+),
+demographics as (
+ select * from acs_tract_with_description
+ union all
+ select * from segregation_indexes
+)
+-- Fill in data for 2011, 2012 using closest available year. Replace 2020 data
+-- with 2019 data to avoid pandemic effects.
+, demographics_replace_years as (
+ select * from demographics where year_ != 2020
+ union all
+ select census_tract, 2020 as year_, name_, description, value_
+ from demographics where year_ = 2019
+ union all
+ select census_tract, 2011 as year_, name_, description, value_
+ from demographics where year_ = 2013
+ union all
+ select census_tract, 2012 as year_, name_, description, value_
+ from demographics where year_ = 2013
+)
+select *
+from demographics_replace_years
From 4cd4b3652014a8130fb4c9337a4502a121a84c2c Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 27 Aug 2024 16:39:50 -0400
Subject: [PATCH 094/142] switch api over to demographics wide table
---
api/schema.sql | 5 ++---
dbt/models/demographics_wide.sql | 34 ++++++++++++++++++++++++++++++++
2 files changed, 36 insertions(+), 3 deletions(-)
create mode 100644 dbt/models/demographics_wide.sql
diff --git a/api/schema.sql b/api/schema.sql
index 7c639ef9..694076f6 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -2,9 +2,8 @@ drop schema if exists api cascade;
create schema api;
-create view api.acs_tract_wide as (
- select * from acs_tract_wide
- order by random()
+create view api.demographics as (
+ select * from demographics_wide
);
drop role if exists web_anon;
diff --git a/dbt/models/demographics_wide.sql b/dbt/models/demographics_wide.sql
new file mode 100644
index 00000000..ca9104bd
--- /dev/null
+++ b/dbt/models/demographics_wide.sql
@@ -0,0 +1,34 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['description']}
+ ]
+ )
+}}
+
+-- This is used by the web app. It has a row for each tract, demographic
+-- variable pair and a column for each year.
+with
+demographics as (select * from {{ ref('demographics') }}),
+census_tracts as (select * from {{ ref('census_tracts_in_city_boundary') }}),
+demographics_filtered as (
+ select demographics.*
+ from demographics
+ inner join census_tracts using (census_tract, year_)
+),
+final_ as (
+ select
+ description,
+ census_tract as tract_id,
+ {{ dbt_utils.pivot('year_',
+ dbt_utils.get_column_values(ref('demographics'),
+ 'year_',
+ order_by='year_'),
+ then_value='value_',
+ else_value='null',
+ agg='max') }}
+ from demographics_filtered
+ group by 1, 2
+)
+select * from final_
From 5ca1ba8cdf4d242f3c48fcc81c7dac137ff66276 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 27 Aug 2024 18:08:39 -0400
Subject: [PATCH 095/142] add census_tracts api endpoint
---
api/schema.sql | 17 ++++++++++++++++-
dbt/models/census_tracts.sql | 1 +
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/api/schema.sql b/api/schema.sql
index 694076f6..85783ad7 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -6,8 +6,23 @@ create view api.demographics as (
select * from demographics_wide
);
-drop role if exists web_anon;
+create view api.census_tracts as (
+ select
+ census_tract,
+ year_,
+ geom
+ from census_tracts
+);
+
+do $$
+begin
create role web_anon nologin;
+exception when duplicate_object then raise notice '%, skipping', sqlerrm using errcode = sqlstate;
+end
+$$;
+
+grant usage on schema public to web_anon;
+grant select on table public.spatial_ref_sys TO web_anon;
grant usage on schema api to web_anon;
grant select on all tables in schema api to web_anon;
grant web_anon to postgres;
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 28ff79e6..496470a9 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -4,6 +4,7 @@
indexes = [
{'columns': ['census_tract_id'], 'unique': true},
{'columns': ['valid', 'geom'], 'type': 'gist'}
+ {'columns': ['year']}
]
)
}}
From ef1fb5b1be40f35316833245a07bc9135d6d5b98 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 28 Aug 2024 14:36:20 -0400
Subject: [PATCH 096/142] use filtered census tracts in api endpoint
---
api/schema.sql | 2 +-
dbt/models/census_tracts.sql | 4 ++--
dbt/models/census_tracts_in_city_boundary.sql | 10 ++++++++++
3 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/api/schema.sql b/api/schema.sql
index 85783ad7..1ee655e0 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -11,7 +11,7 @@ create view api.census_tracts as (
census_tract,
year_,
geom
- from census_tracts
+ from census_tracts_in_city_boundary
);
do $$
diff --git a/dbt/models/census_tracts.sql b/dbt/models/census_tracts.sql
index 496470a9..50462489 100644
--- a/dbt/models/census_tracts.sql
+++ b/dbt/models/census_tracts.sql
@@ -3,8 +3,8 @@
materialized='table',
indexes = [
{'columns': ['census_tract_id'], 'unique': true},
- {'columns': ['valid', 'geom'], 'type': 'gist'}
- {'columns': ['year']}
+ {'columns': ['valid', 'geom'], 'type': 'gist'},
+ {'columns': ['year_']}
]
)
}}
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
index b19de633..266332b0 100644
--- a/dbt/models/census_tracts_in_city_boundary.sql
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -1,3 +1,12 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['year_']}
+ ]
+ )
+}}
+
with census_tracts as (
select * from {{ ref('census_tracts') }}
)
@@ -8,6 +17,7 @@ select
census_tracts.census_tract_id
, census_tracts.census_tract
, census_tracts.year_
+ , census_tracts.geom
from
census_tracts
, city_boundary
From 2912c19ef57da321e6f0a08477aa509d40fd9c8e Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 28 Aug 2024 15:57:04 -0400
Subject: [PATCH 097/142] add dbt to dev depends
---
setup.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/setup.py b/setup.py
index 419f142a..3f14029a 100644
--- a/setup.py
+++ b/setup.py
@@ -25,6 +25,8 @@
"graphviz",
"python-dotenv",
"google-cloud-storage",
+ "dbt-core",
+ "dbt-postgres",
]
setup(
From 027085e3b6c42141e7d0f000a2e0a0648b3a3586 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 28 Aug 2024 15:57:10 -0400
Subject: [PATCH 098/142] add numeric encoding of census tracts
---
dbt/models/census_tracts_wide.sql | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index 05bede3e..eac750b5 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -40,11 +40,17 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
, segregation as (
select * from demographics
where description = 'segregation_index_annual_city'
+),
+census_tract_numeric as (
+ select
+ census_tract
+ , row_number() over () as census_tract_numeric
+ from (select distinct census_tract from census_tracts order by 1)
)
-
, raw_data as (
select
census_tracts.census_tract
+ , census_tract_numeric.census_tract_numeric
, census_tracts.year_
, coalesce(housing_units.num_units, 0) as num_units
, property_values.total_value
@@ -58,6 +64,7 @@ select
, segregation.value_ as segregation
from
census_tracts
+ inner join census_tract_numeric using (census_tract)
inner join housing_units using (census_tract_id)
inner join property_values using (census_tract_id)
inner join distance_to_transit using (census_tract_id)
@@ -70,6 +77,7 @@ from
, with_std as (
select
census_tract
+ , census_tract_numeric
, year_
, num_units
, total_value
From 8a00c91783a24bdc9b143963771c700ba1f66d42 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 28 Aug 2024 16:15:43 -0400
Subject: [PATCH 099/142] use original column names in census_tracts_wide
---
dbt/macros/standardize.sql | 2 +-
dbt/models/census_tracts_wide.sql | 32 +++++++++++--------------------
2 files changed, 12 insertions(+), 22 deletions(-)
diff --git a/dbt/macros/standardize.sql b/dbt/macros/standardize.sql
index 63ec955e..83d71af6 100644
--- a/dbt/macros/standardize.sql
+++ b/dbt/macros/standardize.sql
@@ -1,6 +1,6 @@
{% macro standardize(columns) %}
{% for c in columns %}
- (({{ c }} - (avg({{ c }}) over ())) / (stddev_samp({{ c }}) over ()))::double precision as std_{{ c }}
+ {{ c }} as {{ c }}_original, (({{ c }} - (avg({{ c }}) over ())) / (stddev_samp({{ c }}) over ()))::double precision as {{ c }}
{% if not loop.last %},{% endif %}
{% endfor %}
{% endmacro %}
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index eac750b5..66361721 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -49,14 +49,14 @@ census_tract_numeric as (
)
, raw_data as (
select
- census_tracts.census_tract
- , census_tract_numeric.census_tract_numeric
- , census_tracts.year_
- , coalesce(housing_units.num_units, 0) as num_units
+ census_tracts.census_tract as census_tract_fips
+ , census_tract_numeric.census_tract_numeric as census_tract
+ , census_tracts.year_ as "year"
+ , coalesce(housing_units.num_units, 0) as housing_units
, property_values.total_value
, property_values.median_value
- , distance_to_transit.median_distance_to_transit
- , distance_to_transit.mean_distance_to_transit
+ , distance_to_transit.median_distance_to_transit as median_distance
+ , distance_to_transit.mean_distance_to_transit as mean_distance
, parcel_area.parcel_sqm
, parking_limits.mean_limit
, white_frac.value_ as white
@@ -76,21 +76,11 @@ from
)
, with_std as (
select
- census_tract
- , census_tract_numeric
- , year_
- , num_units
- , total_value
- , median_value
- , median_distance_to_transit
- , mean_distance_to_transit
- , parcel_sqm
- , white
- , income
- , mean_limit
- , segregation
- , {{ standardize(['num_units', 'total_value', 'median_value',
- 'median_distance_to_transit', 'mean_distance_to_transit',
+ census_tract_fips
+ , census_tract
+ , "year"
+ , {{ standardize(['housing_units', 'total_value', 'median_value',
+ 'median_distance', 'mean_distance',
'parcel_sqm', 'white', 'income', 'mean_limit', 'segregation' ]) }}
from
raw_data
From 0ede91404417bd51e73565489469be7e40e71d40 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 28 Aug 2024 16:36:39 -0400
Subject: [PATCH 100/142] add standardize functions for categorical variables
---
dbt/macros/standardize.sql | 9 ++++++++-
dbt/models/census_tracts_wide.sql | 21 ++++++---------------
2 files changed, 14 insertions(+), 16 deletions(-)
diff --git a/dbt/macros/standardize.sql b/dbt/macros/standardize.sql
index 83d71af6..742e971f 100644
--- a/dbt/macros/standardize.sql
+++ b/dbt/macros/standardize.sql
@@ -1,6 +1,13 @@
-{% macro standardize(columns) %}
+{% macro standardize_cont(columns) %}
{% for c in columns %}
{{ c }} as {{ c }}_original, (({{ c }} - (avg({{ c }}) over ())) / (stddev_samp({{ c }}) over ()))::double precision as {{ c }}
{% if not loop.last %},{% endif %}
{% endfor %}
{% endmacro %}
+
+{% macro standardize_cat(columns) %}
+ {% for c in columns %}
+ {{ c }} as {{ c }}_original, (dense_rank() over (order by {{ c }})) - 1 as {{ c }}
+ {% if not loop.last %},{% endif %}
+ {% endfor %}
+{% endmacro %}
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/census_tracts_wide.sql
index 66361721..f9014586 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/census_tracts_wide.sql
@@ -40,17 +40,10 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
, segregation as (
select * from demographics
where description = 'segregation_index_annual_city'
-),
-census_tract_numeric as (
- select
- census_tract
- , row_number() over () as census_tract_numeric
- from (select distinct census_tract from census_tracts order by 1)
)
, raw_data as (
select
- census_tracts.census_tract as census_tract_fips
- , census_tract_numeric.census_tract_numeric as census_tract
+ census_tracts.census_tract::numeric
, census_tracts.year_ as "year"
, coalesce(housing_units.num_units, 0) as housing_units
, property_values.total_value
@@ -64,7 +57,6 @@ select
, segregation.value_ as segregation
from
census_tracts
- inner join census_tract_numeric using (census_tract)
inner join housing_units using (census_tract_id)
inner join property_values using (census_tract_id)
inner join distance_to_transit using (census_tract_id)
@@ -76,12 +68,11 @@ from
)
, with_std as (
select
- census_tract_fips
- , census_tract
- , "year"
- , {{ standardize(['housing_units', 'total_value', 'median_value',
- 'median_distance', 'mean_distance',
- 'parcel_sqm', 'white', 'income', 'mean_limit', 'segregation' ]) }}
+ census_tract::numeric
+ , {{ standardize_cat(['year']) }}
+ , {{ standardize_cont(['housing_units', 'total_value', 'median_value',
+ 'median_distance', 'mean_distance', 'parcel_sqm',
+ 'white', 'income', 'mean_limit', 'segregation' ]) }}
from
raw_data
)
From ca3f8e756103d5b1a37dea21b617ddca7a798796 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 29 Aug 2024 10:19:07 -0400
Subject: [PATCH 101/142] census tracts need to be in 4269 for the api
---
api/schema.sql | 2 +-
dbt/models/census_tracts_api.sql | 16 ++++++++++++++++
dbt/models/census_tracts_in_city_boundary.sql | 9 ---------
3 files changed, 17 insertions(+), 10 deletions(-)
create mode 100644 dbt/models/census_tracts_api.sql
diff --git a/api/schema.sql b/api/schema.sql
index 1ee655e0..3af7fea9 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -11,7 +11,7 @@ create view api.census_tracts as (
census_tract,
year_,
geom
- from census_tracts_in_city_boundary
+ from census_tracts_api
);
do $$
diff --git a/dbt/models/census_tracts_api.sql b/dbt/models/census_tracts_api.sql
new file mode 100644
index 00000000..5208ae44
--- /dev/null
+++ b/dbt/models/census_tracts_api.sql
@@ -0,0 +1,16 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['year_']}
+ ]
+ )
+}}
+
+with census_tracts as (select * from {{ ref('census_tracts_in_city_boundary') }})
+select
+ census_tract
+ , year_
+ , st_transform(geom, 4269) as geom
+from
+ census_tracts
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
index 266332b0..3604fb04 100644
--- a/dbt/models/census_tracts_in_city_boundary.sql
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -1,12 +1,3 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['year_']}
- ]
- )
-}}
-
with census_tracts as (
select * from {{ ref('census_tracts') }}
)
From eccbd849b8ad50db96dc248dd96514c11e9c1f2d Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 29 Aug 2024 11:50:27 -0400
Subject: [PATCH 102/142] reorganize dbt models to support census tract model
---
.../census_tracts_distance_to_transit.sql | 0
.../census_tracts_housing_units.sql | 0
.../census_tracts_parcel_area.sql | 0
.../census_tracts_parking_limits.sql | 8 ++++
.../census_tracts_property_values.sql | 0
.../parcels_distance_to_transit.sql | 13 +----
.../intermediate/parcels_parking_limits.sql} | 47 ++++++++-----------
.../tracts_model__census_tracts.sql} | 2 +
.../tracts_model/tracts_model__parcels.sql | 23 +++++++++
9 files changed, 54 insertions(+), 39 deletions(-)
rename dbt/models/{ => tracts_model/intermediate}/census_tracts_distance_to_transit.sql (100%)
rename dbt/models/{ => tracts_model/intermediate}/census_tracts_housing_units.sql (100%)
rename dbt/models/{ => tracts_model/intermediate}/census_tracts_parcel_area.sql (100%)
create mode 100644 dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql
rename dbt/models/{ => tracts_model/intermediate}/census_tracts_property_values.sql (100%)
rename dbt/models/{ => tracts_model/intermediate}/parcels_distance_to_transit.sql (74%)
rename dbt/models/{census_tracts_parking_limits.sql => tracts_model/intermediate/parcels_parking_limits.sql} (54%)
rename dbt/models/{census_tracts_wide.sql => tracts_model/tracts_model__census_tracts.sql} (98%)
create mode 100644 dbt/models/tracts_model/tracts_model__parcels.sql
diff --git a/dbt/models/census_tracts_distance_to_transit.sql b/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
similarity index 100%
rename from dbt/models/census_tracts_distance_to_transit.sql
rename to dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
diff --git a/dbt/models/census_tracts_housing_units.sql b/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
similarity index 100%
rename from dbt/models/census_tracts_housing_units.sql
rename to dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
diff --git a/dbt/models/census_tracts_parcel_area.sql b/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
similarity index 100%
rename from dbt/models/census_tracts_parcel_area.sql
rename to dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql b/dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql
new file mode 100644
index 00000000..430e5fd6
--- /dev/null
+++ b/dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql
@@ -0,0 +1,8 @@
+with
+parcels_parking_limits as (select * from {{ ref('parcels_parking_limits') }}),
+parcels as (select * from {{ ref('parcels') }})
+select
+ census_tract_id,
+ avg(limit_numeric) as mean_limit
+from parcels join parcels_parking_limits using (parcel_id)
+group by census_tract_id
diff --git a/dbt/models/census_tracts_property_values.sql b/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
similarity index 100%
rename from dbt/models/census_tracts_property_values.sql
rename to dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
diff --git a/dbt/models/parcels_distance_to_transit.sql b/dbt/models/tracts_model/intermediate/parcels_distance_to_transit.sql
similarity index 74%
rename from dbt/models/parcels_distance_to_transit.sql
rename to dbt/models/tracts_model/intermediate/parcels_distance_to_transit.sql
index 6580ca58..eb543f29 100644
--- a/dbt/models/parcels_distance_to_transit.sql
+++ b/dbt/models/tracts_model/intermediate/parcels_distance_to_transit.sql
@@ -1,12 +1,3 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['parcel_id'], 'unique': true},
- ]
- )
-}}
-
-- This model calculates the distance from each parcel to the nearest high
-- frequency transit line or stop
with
@@ -21,8 +12,8 @@ with
lines inner join stops on lines.valid && stops.valid
)
select
- parcels.parcel_id
- , st_distance(parcels.geom, lines_and_stops.geom) as distance
+ parcels.parcel_id,
+ st_distance(parcels.geom, lines_and_stops.geom) as distance
from
parcels
inner join lines_and_stops on parcels.valid && lines_and_stops.valid
diff --git a/dbt/models/census_tracts_parking_limits.sql b/dbt/models/tracts_model/intermediate/parcels_parking_limits.sql
similarity index 54%
rename from dbt/models/census_tracts_parking_limits.sql
rename to dbt/models/tracts_model/intermediate/parcels_parking_limits.sql
index be6a25f1..3436ae30 100644
--- a/dbt/models/census_tracts_parking_limits.sql
+++ b/dbt/models/tracts_model/intermediate/parcels_parking_limits.sql
@@ -1,22 +1,21 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['census_tract_id'], 'unique': true},
- ]
- )
-}}
-
with
parcels as (select * from {{ ref('parcels') }}),
transit as (select * from {{ ref('high_frequency_transit_lines') }}),
downtown as (select * from {{ ref('downtown') }}),
-with_parking_limit as (
+with_is_downtown as (
select
- parcel_id,
- census_tract_id,
+ parcels.parcel_id,
+ parcels.valid,
+ parcels.geom,
+ st_intersects(parcels.geom, downtown.geom) as is_downtown
+ from downtown, parcels
+),
+with_limit as (
+ select
+ parcels.parcel_id,
+ parcels.is_downtown,
case
- when st_intersects(parcels.geom, downtown.geom) then 'eliminated'
+ when parcels.is_downtown then 'eliminated'
when parcels.valid << '[2015-01-01,)'::daterange then 'full'
else
case
@@ -26,27 +25,19 @@ with_parking_limit as (
end
end as limit_
from
- downtown, parcels
- left join transit
- on parcels.valid && transit.valid
+ with_is_downtown as parcels
+ join transit on parcels.valid && transit.valid
),
with_limit_numeric as (
select
- parcel_id,
- census_tract_id,
- limit_,
+ parcels.parcel_id,
+ parcels.is_downtown,
+ parcels.limit_,
case limit_
when 'full' then 1
when 'reduced' then 0.5
when 'eliminated' then 0
end as limit_numeric
- from with_parking_limit
-),
-by_census_tract as (
- select
- census_tract_id,
- avg(limit_numeric) as mean_limit
- from with_limit_numeric
- group by census_tract_id
+ from with_limit as parcels
)
-select * from by_census_tract
+select * from with_limit_numeric
diff --git a/dbt/models/census_tracts_wide.sql b/dbt/models/tracts_model/tracts_model__census_tracts.sql
similarity index 98%
rename from dbt/models/census_tracts_wide.sql
rename to dbt/models/tracts_model/tracts_model__census_tracts.sql
index f9014586..f00225a4 100644
--- a/dbt/models/census_tracts_wide.sql
+++ b/dbt/models/tracts_model/tracts_model__census_tracts.sql
@@ -12,6 +12,7 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
, demographics as (select * from {{ ref('demographics') }})
+, downtown as (select * from {{ ref('downtown') }})
, census_tracts as (
select *
from {{ ref('census_tracts') }}
@@ -41,6 +42,7 @@ in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
select * from demographics
where description = 'segregation_index_annual_city'
)
+
, raw_data as (
select
census_tracts.census_tract::numeric
diff --git a/dbt/models/tracts_model/tracts_model__parcels.sql b/dbt/models/tracts_model/tracts_model__parcels.sql
new file mode 100644
index 00000000..c2f1eecb
--- /dev/null
+++ b/dbt/models/tracts_model/tracts_model__parcels.sql
@@ -0,0 +1,23 @@
+{{
+ config(
+ materialized='table',
+ )
+}}
+
+with
+parcels_parking_limits as (select * from {{ ref('parcels_parking_limits') }}),
+parcels_distance_to_transit as (select * from {{ ref('parcels_distance_to_transit') }}),
+parcels as (select * from {{ ref('parcels') }}),
+census_tracts as (select * from {{ ref('census_tracts') }})
+select
+ parcels.pin,
+ census_tracts.census_tract,
+ census_tracts.year_,
+ parcels_distance_to_transit.distance as distance_to_transit,
+ parcels_parking_limits.limit_numeric as limit_con,
+ parcels_parking_limits.is_downtown as downtown_yn
+from
+ parcels
+ join parcels_parking_limits using (parcel_id)
+ join parcels_distance_to_transit using (parcel_id)
+ join census_tracts using (census_tract_id)
From 1fdbf2b14b7151b234215de93277840475be770b Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 29 Aug 2024 15:53:36 -0400
Subject: [PATCH 103/142] refactor
---
dbt/models/census_tracts_in_city_boundary.sql | 2 +-
.../census_tracts_distance_to_transit.sql | 25 ++++++-------------
.../census_tracts_housing_units.sql | 20 +++------------
.../census_tracts_parcel_area.sql | 16 +++---------
.../census_tracts_property_values.sql | 23 +++++------------
.../tracts_model__census_tracts.sql | 10 +++-----
.../tracts_model/tracts_model__parcels.sql | 4 +--
7 files changed, 27 insertions(+), 73 deletions(-)
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
index 3604fb04..18a8d773 100644
--- a/dbt/models/census_tracts_in_city_boundary.sql
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -13,4 +13,4 @@ from
census_tracts
, city_boundary
where st_intersects(census_tracts.geom, city_boundary.geom)
- and st_area(st_intersection(census_tracts.geom, city_boundary.geom)) / st_area(census_tracts.geom) > 0.2
+ and st_area(st_intersection(census_tracts.geom, city_boundary.geom)) / st_area(census_tracts.geom) > 0.9
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql b/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
index edf28211..abe15828 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
@@ -1,24 +1,15 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['census_tract_id'], 'unique': true},
- ]
- )
-}}
-
with
parcels_distance_to_transit as (
select * from {{ ref('parcels_distance_to_transit') }}
- )
- , census_tracts as (select * from {{ ref('census_tracts') }})
- , parcels as (select * from {{ ref('parcels') }})
+ ),
+ census_tracts as (select * from {{ ref('census_tracts') }}),
+ parcels as (select * from {{ ref('parcels') }})
select
- census_tracts.census_tract_id
- , avg(parcels_distance_to_transit.distance) as mean_distance_to_transit
- , {{ median('parcels_distance_to_transit.distance') }} as median_distance_to_transit
+ census_tracts.census_tract_id,
+ avg(parcels_distance_to_transit.distance) as mean_distance_to_transit,
+ {{ median('parcels_distance_to_transit.distance') }} as median_distance_to_transit
from
census_tracts
- left join parcels using (census_tract_id)
- left join parcels_distance_to_transit using (parcel_id)
+ left join parcels using (census_tract_id)
+ left join parcels_distance_to_transit using (parcel_id)
group by 1
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql b/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
index 38c91359..0b5aa907 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
@@ -1,19 +1,7 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['census_tract_id'], 'unique': true},
- ]
- )
-}}
-
-with census_tracts as (
- select * from {{ ref('census_tracts') }}
-)
-, residential_permits as (
- select * from {{ ref('residential_permits') }}
-)
-, residential_permits_to_census_tracts as (
+with
+census_tracts as (select * from {{ ref('census_tracts') }}),
+residential_permits as (select * from {{ ref('residential_permits') }}),
+residential_permits_to_census_tracts as (
select * from {{ ref('residential_permits_to_census_tracts') }}
)
select
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql b/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
index 687d2274..d2e9b5d5 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
@@ -1,19 +1,9 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['census_tract_id'], 'unique': true},
- ]
- )
-}}
-
with
census_tracts as (select * from {{ ref('census_tracts') }}),
parcels as (select * from {{ ref('parcels') }})
select
- census_tract_id
- , sum(st_area(parcels.geom)) as parcel_sqm
+ census_tract_id,
+ sum(st_area(parcels.geom)) as parcel_sqm
from
- census_tracts
- left join parcels using (census_tract_id)
+ census_tracts left join parcels using (census_tract_id)
group by 1
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql b/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
index 7bb18e72..60cf69c9 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
@@ -1,22 +1,11 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['census_tract_id'], 'unique': true},
- ]
- )
-}}
-
-- Median and total parcel property values aggregated by census tract.
-
with
-parcels as (select * from {{ ref('parcels') }})
-, census_tracts as (select * from {{ ref('census_tracts') }})
+parcels as (select * from {{ ref('parcels') }}),
+census_tracts as (select * from {{ ref('census_tracts') }})
select
- census_tracts.census_tract_id
- , sum(parcels.emv_total) as total_value
- , {{ median('parcels.emv_total') }} as median_value
+ census_tracts.census_tract_id,
+ sum(parcels.emv_total) as total_value,
+ {{ median('parcels.emv_total') }} as median_value
from
- census_tracts
- left join parcels using (census_tract_id)
+ census_tracts left join parcels using (census_tract_id)
group by 1
diff --git a/dbt/models/tracts_model/tracts_model__census_tracts.sql b/dbt/models/tracts_model/tracts_model__census_tracts.sql
index f00225a4..a2c31c40 100644
--- a/dbt/models/tracts_model/tracts_model__census_tracts.sql
+++ b/dbt/models/tracts_model/tracts_model__census_tracts.sql
@@ -5,20 +5,16 @@
}}
with
-in_city_boundary as (select * from {{ ref('census_tracts_in_city_boundary') }})
-, housing_units as (select * from {{ ref('census_tracts_housing_units') }})
+housing_units as (select * from {{ ref('census_tracts_housing_units') }})
, property_values as (select * from {{ ref('census_tracts_property_values') }})
, distance_to_transit as (select * from {{ ref('census_tracts_distance_to_transit') }})
, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
, demographics as (select * from {{ ref('demographics') }})
-, downtown as (select * from {{ ref('downtown') }})
, census_tracts as (
select *
- from {{ ref('census_tracts') }}
- where
- year_ <= 2020
- and census_tract_id in (select census_tract_id from in_city_boundary)
+ from {{ ref('census_tracts_in_city_boundary') }}
+ where year_ <= 2020
)
-- Demographic data
diff --git a/dbt/models/tracts_model/tracts_model__parcels.sql b/dbt/models/tracts_model/tracts_model__parcels.sql
index c2f1eecb..92e471eb 100644
--- a/dbt/models/tracts_model/tracts_model__parcels.sql
+++ b/dbt/models/tracts_model/tracts_model__parcels.sql
@@ -8,9 +8,9 @@ with
parcels_parking_limits as (select * from {{ ref('parcels_parking_limits') }}),
parcels_distance_to_transit as (select * from {{ ref('parcels_distance_to_transit') }}),
parcels as (select * from {{ ref('parcels') }}),
-census_tracts as (select * from {{ ref('census_tracts') }})
+census_tracts as (select * from {{ ref('census_tracts_in_city_boundary') }})
select
- parcels.pin,
+ parcels.*,
census_tracts.census_tract,
census_tracts.year_,
parcels_distance_to_transit.distance as distance_to_transit,
From cfa9713b9d1594a2545c5abf6f61fcf93e141af4 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 29 Aug 2024 16:33:44 -0400
Subject: [PATCH 104/142] move tract filters to shared view
---
.../intermediate/census_tracts_distance_to_transit.sql | 10 +++-------
.../intermediate/census_tracts_housing_units.sql | 2 +-
.../intermediate/census_tracts_parcel_area.sql | 4 ++--
.../intermediate/census_tracts_parking_limits.sql | 6 +++---
.../intermediate/census_tracts_property_values.sql | 4 ++--
.../intermediate/parcels_distance_to_transit.sql | 3 ++-
.../intermediate/parcels_parking_limits.sql | 5 ++++-
.../tracts_model_int__census_tracts_filtered.sql | 3 +++
.../tracts_model_int__parcels_filtered.sql | 4 ++++
.../tracts_model/tracts_model__census_tracts.sql | 6 +-----
dbt/models/tracts_model/tracts_model__parcels.sql | 6 +++---
11 files changed, 28 insertions(+), 25 deletions(-)
create mode 100644 dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
create mode 100644 dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql b/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
index abe15828..a25c6005 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_distance_to_transit.sql
@@ -1,15 +1,11 @@
with
- parcels_distance_to_transit as (
- select * from {{ ref('parcels_distance_to_transit') }}
- ),
- census_tracts as (select * from {{ ref('census_tracts') }}),
- parcels as (select * from {{ ref('parcels') }})
+parcels_distance_to_transit as (select * from {{ ref('parcels_distance_to_transit') }}),
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }})
select
census_tracts.census_tract_id,
avg(parcels_distance_to_transit.distance) as mean_distance_to_transit,
{{ median('parcels_distance_to_transit.distance') }} as median_distance_to_transit
from
census_tracts
- left join parcels using (census_tract_id)
- left join parcels_distance_to_transit using (parcel_id)
+ left join parcels_distance_to_transit using (census_tract_id)
group by 1
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql b/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
index 0b5aa907..e0654c55 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
@@ -1,5 +1,5 @@
with
-census_tracts as (select * from {{ ref('census_tracts') }}),
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }}),
residential_permits as (select * from {{ ref('residential_permits') }}),
residential_permits_to_census_tracts as (
select * from {{ ref('residential_permits_to_census_tracts') }}
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql b/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
index d2e9b5d5..cb6760fe 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
@@ -1,6 +1,6 @@
with
-census_tracts as (select * from {{ ref('census_tracts') }}),
-parcels as (select * from {{ ref('parcels') }})
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }}),
+parcels as (select * from {{ ref('tracts_model_int__parcels_filtered') }})
select
census_tract_id,
sum(st_area(parcels.geom)) as parcel_sqm
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql b/dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql
index 430e5fd6..cf99bf05 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_parking_limits.sql
@@ -1,8 +1,8 @@
with
-parcels_parking_limits as (select * from {{ ref('parcels_parking_limits') }}),
-parcels as (select * from {{ ref('parcels') }})
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }}),
+parcels_parking_limits as (select * from {{ ref('parcels_parking_limits') }})
select
census_tract_id,
avg(limit_numeric) as mean_limit
-from parcels join parcels_parking_limits using (parcel_id)
+from census_tracts left join parcels_parking_limits using (census_tract_id)
group by census_tract_id
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql b/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
index 60cf69c9..71f8b74a 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_property_values.sql
@@ -1,7 +1,7 @@
-- Median and total parcel property values aggregated by census tract.
with
-parcels as (select * from {{ ref('parcels') }}),
-census_tracts as (select * from {{ ref('census_tracts') }})
+parcels as (select * from {{ ref('tracts_model_int__parcels_filtered') }}),
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }})
select
census_tracts.census_tract_id,
sum(parcels.emv_total) as total_value,
diff --git a/dbt/models/tracts_model/intermediate/parcels_distance_to_transit.sql b/dbt/models/tracts_model/intermediate/parcels_distance_to_transit.sql
index eb543f29..18cdbf48 100644
--- a/dbt/models/tracts_model/intermediate/parcels_distance_to_transit.sql
+++ b/dbt/models/tracts_model/intermediate/parcels_distance_to_transit.sql
@@ -1,7 +1,7 @@
-- This model calculates the distance from each parcel to the nearest high
-- frequency transit line or stop
with
- parcels as (select * from {{ ref('parcels') }})
+ parcels as (select * from {{ ref('tracts_model_int__parcels_filtered') }})
, lines as (select * from {{ ref('high_frequency_transit_lines') }})
, stops as (select * from {{ ref('high_frequency_transit_stops') }})
, lines_and_stops as materialized (
@@ -13,6 +13,7 @@ with
)
select
parcels.parcel_id,
+ parcels.census_tract_id,
st_distance(parcels.geom, lines_and_stops.geom) as distance
from
parcels
diff --git a/dbt/models/tracts_model/intermediate/parcels_parking_limits.sql b/dbt/models/tracts_model/intermediate/parcels_parking_limits.sql
index 3436ae30..aebd7b00 100644
--- a/dbt/models/tracts_model/intermediate/parcels_parking_limits.sql
+++ b/dbt/models/tracts_model/intermediate/parcels_parking_limits.sql
@@ -1,10 +1,11 @@
with
-parcels as (select * from {{ ref('parcels') }}),
+parcels as (select * from {{ ref('tracts_model_int__parcels_filtered') }}),
transit as (select * from {{ ref('high_frequency_transit_lines') }}),
downtown as (select * from {{ ref('downtown') }}),
with_is_downtown as (
select
parcels.parcel_id,
+ parcels.census_tract_id,
parcels.valid,
parcels.geom,
st_intersects(parcels.geom, downtown.geom) as is_downtown
@@ -13,6 +14,7 @@ with_is_downtown as (
with_limit as (
select
parcels.parcel_id,
+ parcels.census_tract_id,
parcels.is_downtown,
case
when parcels.is_downtown then 'eliminated'
@@ -31,6 +33,7 @@ with_limit as (
with_limit_numeric as (
select
parcels.parcel_id,
+ parcels.census_tract_id,
parcels.is_downtown,
parcels.limit_,
case limit_
diff --git a/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql b/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
new file mode 100644
index 00000000..7bd1a884
--- /dev/null
+++ b/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
@@ -0,0 +1,3 @@
+select *
+from {{ ref('census_tracts_in_city_boundary') }}
+where year_ <= 2020
diff --git a/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
new file mode 100644
index 00000000..30fe050c
--- /dev/null
+++ b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
@@ -0,0 +1,4 @@
+with
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }})
+select parcels.*
+from {{ ref('parcels') }} join census_tracts using (census_tract_id)
diff --git a/dbt/models/tracts_model/tracts_model__census_tracts.sql b/dbt/models/tracts_model/tracts_model__census_tracts.sql
index a2c31c40..77131d20 100644
--- a/dbt/models/tracts_model/tracts_model__census_tracts.sql
+++ b/dbt/models/tracts_model/tracts_model__census_tracts.sql
@@ -11,11 +11,7 @@ housing_units as (select * from {{ ref('census_tracts_housing_units') }})
, parcel_area as (select * from {{ ref('census_tracts_parcel_area') }})
, parking_limits as (select * from {{ ref('census_tracts_parking_limits') }})
, demographics as (select * from {{ ref('demographics') }})
-, census_tracts as (
- select *
- from {{ ref('census_tracts_in_city_boundary') }}
- where year_ <= 2020
-)
+, census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }})
-- Demographic data
, white as (
diff --git a/dbt/models/tracts_model/tracts_model__parcels.sql b/dbt/models/tracts_model/tracts_model__parcels.sql
index 92e471eb..d11f4605 100644
--- a/dbt/models/tracts_model/tracts_model__parcels.sql
+++ b/dbt/models/tracts_model/tracts_model__parcels.sql
@@ -7,8 +7,8 @@
with
parcels_parking_limits as (select * from {{ ref('parcels_parking_limits') }}),
parcels_distance_to_transit as (select * from {{ ref('parcels_distance_to_transit') }}),
-parcels as (select * from {{ ref('parcels') }}),
-census_tracts as (select * from {{ ref('census_tracts_in_city_boundary') }})
+parcels as (select * from {{ ref('tracts_model_int__parcels_filtered') }}),
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }})
select
parcels.*,
census_tracts.census_tract,
@@ -18,6 +18,6 @@ select
parcels_parking_limits.is_downtown as downtown_yn
from
parcels
+ join census_tracts using (census_tract_id)
join parcels_parking_limits using (parcel_id)
join parcels_distance_to_transit using (parcel_id)
- join census_tracts using (census_tract_id)
From d602f9c7fdfee43853a395c91902ae9c138c8b18 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 29 Aug 2024 16:50:24 -0400
Subject: [PATCH 105/142] filter out data before 2011, because we don't have
demographics
---
.../intermediate/tracts_model_int__census_tracts_filtered.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql b/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
index 7bd1a884..28656986 100644
--- a/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
+++ b/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
@@ -1,3 +1,3 @@
select *
from {{ ref('census_tracts_in_city_boundary') }}
-where year_ <= 2020
+where 2010 < year_ and year_ <= 2020
From 08cc805840d9c96ea9f91775426b2d097d84661e Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 30 Aug 2024 09:48:44 -0400
Subject: [PATCH 106/142] reorganize api and add high frequency transit
---
api/schema.sql | 12 ++++++------
.../api__census_tracts.sql} | 0
.../api__demographics.sql} | 0
.../api/api__high_frequency_transit_lines.sql | 17 +++++++++++++++++
4 files changed, 23 insertions(+), 6 deletions(-)
rename dbt/models/{census_tracts_api.sql => api/api__census_tracts.sql} (100%)
rename dbt/models/{demographics_wide.sql => api/api__demographics.sql} (100%)
create mode 100644 dbt/models/api/api__high_frequency_transit_lines.sql
diff --git a/api/schema.sql b/api/schema.sql
index 3af7fea9..42fd4e4c 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -3,15 +3,15 @@ drop schema if exists api cascade;
create schema api;
create view api.demographics as (
- select * from demographics_wide
+ select * from api__demographics
);
create view api.census_tracts as (
- select
- census_tract,
- year_,
- geom
- from census_tracts_api
+ select * from api__census_tracts
+);
+
+create view api.high_frequency_transit_lines as (
+ select * from api__high_frequency_transit_lines
);
do $$
diff --git a/dbt/models/census_tracts_api.sql b/dbt/models/api/api__census_tracts.sql
similarity index 100%
rename from dbt/models/census_tracts_api.sql
rename to dbt/models/api/api__census_tracts.sql
diff --git a/dbt/models/demographics_wide.sql b/dbt/models/api/api__demographics.sql
similarity index 100%
rename from dbt/models/demographics_wide.sql
rename to dbt/models/api/api__demographics.sql
diff --git a/dbt/models/api/api__high_frequency_transit_lines.sql b/dbt/models/api/api__high_frequency_transit_lines.sql
new file mode 100644
index 00000000..d48fb342
--- /dev/null
+++ b/dbt/models/api/api__high_frequency_transit_lines.sql
@@ -0,0 +1,17 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['valid']}
+ ]
+ )
+}}
+
+select
+ high_frequency_transit_line_id,
+ valid,
+ geom,
+ blue_zone_geom,
+ yellow_zone_geom
+from
+ {{ ref('high_frequency_transit_lines') }}
From 68de90becbe1d6a103b83164a1824af3e1634ae1 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 30 Aug 2024 13:20:17 -0400
Subject: [PATCH 107/142] replace 2020 census tract geometry with 2019
- mirrors the replacement of demographic data
- requires parcels & residential permits to be retagged, since census_tract_id changes
---
dbt/models/census_tracts_in_city_boundary.sql | 1 +
.../census_tracts_housing_units.sql | 22 ++++++++++--
...acts_model_int__census_tracts_filtered.sql | 36 +++++++++++++++++--
.../tracts_model_int__parcels_filtered.sql | 24 +++++++++++--
.../tracts_model__census_tracts.sql | 2 +-
5 files changed, 75 insertions(+), 10 deletions(-)
diff --git a/dbt/models/census_tracts_in_city_boundary.sql b/dbt/models/census_tracts_in_city_boundary.sql
index 18a8d773..5a2955fc 100644
--- a/dbt/models/census_tracts_in_city_boundary.sql
+++ b/dbt/models/census_tracts_in_city_boundary.sql
@@ -6,6 +6,7 @@ with census_tracts as (
)
select
census_tracts.census_tract_id
+ , census_tracts.valid
, census_tracts.census_tract
, census_tracts.year_
, census_tracts.geom
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql b/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
index e0654c55..42033743 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_housing_units.sql
@@ -2,11 +2,27 @@ with
census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }}),
residential_permits as (select * from {{ ref('residential_permits') }}),
residential_permits_to_census_tracts as (
- select * from {{ ref('residential_permits_to_census_tracts') }}
+ with
+ residential_permits_tag as (
+ select
+ residential_permit_id as id
+ , daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
+ , geom
+ from residential_permits
+ ),
+ census_tracts_tag as (
+ select census_tract_id as id, valid, geom from census_tracts
+ )
+ select
+ child_id as residential_permit_id,
+ parent_id as census_tract_id,
+ valid,
+ type_
+ from {{ tag_regions("residential_permits_tag", "census_tracts_tag") }}
)
select
- census_tracts.census_tract_id
- , sum(residential_permits.num_units)::int as num_units
+ census_tracts.census_tract_id,
+ sum(residential_permits.num_units)::int as num_units
from
census_tracts
left join residential_permits_to_census_tracts using (census_tract_id)
diff --git a/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql b/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
index 28656986..eeb99fcd 100644
--- a/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
+++ b/dbt/models/tracts_model/intermediate/tracts_model_int__census_tracts_filtered.sql
@@ -1,3 +1,33 @@
-select *
-from {{ ref('census_tracts_in_city_boundary') }}
-where 2010 < year_ and year_ <= 2020
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
+-- Consider only tracts in the city boundary, replace 2020 tracts with 2019
+-- tracts, and regenerate the surrogate key.
+with census_tracts_in_city_boundary as (
+ select *
+ from {{ ref('census_tracts_in_city_boundary') }}
+ where 2010 < year_ and year_ < 2020
+),
+census_tracts_union as (
+select census_tract, year_, valid, geom from census_tracts_in_city_boundary
+union all
+select
+ census_tract,
+ 2020 as year_,
+ '[2020-01-01,2021-01-01)'::daterange as valid,
+ geom
+from census_tracts_in_city_boundary where year_ = 2019
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['census_tract', 'year_']) }} as census_tract_id,
+ census_tract,
+ year_,
+ valid,
+ geom
+from census_tracts_union
diff --git a/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
index 30fe050c..f14ca0fc 100644
--- a/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
+++ b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
@@ -1,4 +1,22 @@
+{{
+ config(
+ materialized='table'
+ )
+}}
+
+-- Retag parcels with census tracts (because we replaced the 2020 tracts with the 2019 tracts)
with
-census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }})
-select parcels.*
-from {{ ref('parcels') }} join census_tracts using (census_tract_id)
+census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }}),
+parcels as (select * from {{ ref('parcels_base') }}),
+
+parcels_tag as (select parcel_id as id, valid, geom from parcels),
+census_tracts_tag as (select census_tract_id as id, valid, geom from census_tracts),
+parcels_to_census_tracts as (
+ select
+ child_id as parcel_id,
+ parent_id as census_tract_id
+ from {{ tag_regions("parcels_tag", "census_tracts_tag") }}
+)
+
+select parcels.*, parcels_to_census_tracts.census_tract_id
+from parcels join parcels_to_census_tracts using (parcel_id)
diff --git a/dbt/models/tracts_model/tracts_model__census_tracts.sql b/dbt/models/tracts_model/tracts_model__census_tracts.sql
index 77131d20..dea70ec5 100644
--- a/dbt/models/tracts_model/tracts_model__census_tracts.sql
+++ b/dbt/models/tracts_model/tracts_model__census_tracts.sql
@@ -56,7 +56,7 @@ from
inner join distance_to_transit using (census_tract_id)
inner join parcel_area using (census_tract_id)
inner join parking_limits using (census_tract_id)
- inner join segregation using (census_tract, year_)
+ left join segregation using (census_tract, year_)
left join white_frac using (census_tract, year_)
left join income using (census_tract, year_)
)
From 9da68434ad17d6760baebd766182d6c4076be196 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 30 Aug 2024 15:18:45 -0400
Subject: [PATCH 108/142] fix srid for api
---
dbt/models/api/api__high_frequency_transit_lines.sql | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/dbt/models/api/api__high_frequency_transit_lines.sql b/dbt/models/api/api__high_frequency_transit_lines.sql
index d48fb342..3e445e5b 100644
--- a/dbt/models/api/api__high_frequency_transit_lines.sql
+++ b/dbt/models/api/api__high_frequency_transit_lines.sql
@@ -10,8 +10,8 @@
select
high_frequency_transit_line_id,
valid,
- geom,
- blue_zone_geom,
- yellow_zone_geom
+ st_transform(geom, 4269) as geom,
+ st_transform(blue_zone_geom, 4269) as blue_zone_geom,
+ st_transform(yellow_zone_geom, 4269) as yellow_zone_geom
from
{{ ref('high_frequency_transit_lines') }}
From 2de2b05d2111001201e1da99796f4a9b51f90680 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 10:46:03 -0400
Subject: [PATCH 109/142] type conversion
---
dbt/models/tracts_model/tracts_model__census_tracts.sql | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/dbt/models/tracts_model/tracts_model__census_tracts.sql b/dbt/models/tracts_model/tracts_model__census_tracts.sql
index dea70ec5..8d584472 100644
--- a/dbt/models/tracts_model/tracts_model__census_tracts.sql
+++ b/dbt/models/tracts_model/tracts_model__census_tracts.sql
@@ -37,15 +37,15 @@ housing_units as (select * from {{ ref('census_tracts_housing_units') }})
, raw_data as (
select
- census_tracts.census_tract::numeric
- , census_tracts.year_ as "year"
+ census_tracts.census_tract::bigint
+ , census_tracts.year_::smallint as "year"
, coalesce(housing_units.num_units, 0) as housing_units
, property_values.total_value
, property_values.median_value
, distance_to_transit.median_distance_to_transit as median_distance
, distance_to_transit.mean_distance_to_transit as mean_distance
, parcel_area.parcel_sqm
- , parking_limits.mean_limit
+ , parking_limits.mean_limit::double precision
, white_frac.value_ as white
, income.value_ as income
, segregation.value_ as segregation
@@ -62,7 +62,7 @@ from
)
, with_std as (
select
- census_tract::numeric
+ census_tract
, {{ standardize_cat(['year']) }}
, {{ standardize_cont(['housing_units', 'total_value', 'median_value',
'median_distance', 'mean_distance', 'parcel_sqm',
From affab56961d585b1ba5875488d85e5c88833b7d0 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 16:00:33 -0400
Subject: [PATCH 110/142] simplify acs models
---
dbt/models/acs_block_group.sql | 19 ++++--------
dbt/models/acs_block_group_clean.sql | 10 -------
dbt/models/acs_tract.sql | 15 ++++------
dbt/models/acs_tract_clean.sql | 20 -------------
dbt/models/acs_tract_wide.sql | 45 ----------------------------
5 files changed, 11 insertions(+), 98 deletions(-)
delete mode 100644 dbt/models/acs_block_group_clean.sql
delete mode 100644 dbt/models/acs_tract_clean.sql
delete mode 100644 dbt/models/acs_tract_wide.sql
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index 37d6f96e..ea77a2b4 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -2,21 +2,14 @@
config(
materialized='table',
indexes = [
- {'columns': ['census_block_group_id', 'year_', 'name_'], 'unique': true},
+ {'columns': ['census_block_group', 'year_', 'name_'], 'unique': true},
]
)
}}
-with
-census_block_groups as (select * from {{ ref('census_block_groups') }})
-, acs_bg as (select * from {{ ref('acs_block_group_clean') }})
select
- census_block_groups.census_block_group_id
- , acs_bg.year_
- , acs_bg.name_
- , acs_bg.value_
-from
- acs_bg
- inner join census_block_groups using (statefp, countyfp, tractce, blkgrpce)
-where
- to_date(acs_bg.year_::text , 'YYYY') <@ census_block_groups.valid
+ year::smallint as year_,
+ code as name_,
+ statefp || countyfp || tractce || blkgrpce as census_block_group,
+ case when "value" < 0 then null else "value" end as value_
+from {{ source('minneapolis', 'acs_bg_raw') }}
diff --git a/dbt/models/acs_block_group_clean.sql b/dbt/models/acs_block_group_clean.sql
deleted file mode 100644
index 22cf94e4..00000000
--- a/dbt/models/acs_block_group_clean.sql
+++ /dev/null
@@ -1,10 +0,0 @@
-select
- statefp
- , countyfp
- , tractce
- , blkgrpce
- , year as year_
- , code as name_
- , case when "value" < 0 then null else "value" end as value_
-from
- {{ source('minneapolis', 'acs_bg_raw') }}
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index 96fc0a02..3a4d1b74 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -7,14 +7,9 @@
)
}}
-with
-census_tracts as (select * from {{ ref("census_tracts") }})
-, acs_tract as (select * from {{ ref('acs_tract_clean') }})
select
- census_tract
- , acs_tract.year_
- , acs_tract.name_
- , acs_tract.value_
-from
- acs_tract
- inner join census_tracts using (statefp, countyfp, tractce, year_)
+ year::smallint as year_,
+ code as name_,
+ statefp || countyfp || tractce as census_tract,
+ case when "value" < 0 then null else "value" end as value_
+from {{ source('minneapolis', 'acs_tract_raw') }}
diff --git a/dbt/models/acs_tract_clean.sql b/dbt/models/acs_tract_clean.sql
deleted file mode 100644
index 1c631ff4..00000000
--- a/dbt/models/acs_tract_clean.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-with
-acs_tract_raw as (
- select
- statefp
- , countyfp
- , tractce
- , year
- , code
- , value
- from {{ source('minneapolis', 'acs_tract_raw') }}
-)
-select
- statefp
- , countyfp
- , tractce
- , year as year_
- , code as name_
- , case when "value" < 0 then null else "value" end as value_
-from
- acs_tract_raw
diff --git a/dbt/models/acs_tract_wide.sql b/dbt/models/acs_tract_wide.sql
deleted file mode 100644
index 543c38e7..00000000
--- a/dbt/models/acs_tract_wide.sql
+++ /dev/null
@@ -1,45 +0,0 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['description']}
- ]
- )
-}}
-
-{% set years = range(2013, 2023) %}
-
-with
-acs_tract as (select * from {{ ref('acs_tract') }})
-, acs_variables as (select * from {{ ref('acs_variables') }})
-, census_tracts as (select * from {{ ref('census_tracts_in_city_boundary') }})
-, acs_tract_filtered as (
- select acs_tract.*, description
- from acs_tract
- inner join census_tracts using (census_tract, year_)
- inner join acs_variables on acs_tract.name_ = acs_variables.variable
-)
-, distinct_tracts_and_variables as (
- select distinct
- census_tract
- , name_
- , description
- from acs_tract_filtered
-)
-select
- description
- , census_tract as tract_id
-{% for year_ in years %}
- , "{{ year_ }}"
-{% endfor %}
-from distinct_tracts_and_variables
-{% for year_ in years %}
-left join
-(select
- census_tract
- , name_
- , value_ as "{{ year_}}"
-from acs_tract_filtered
-where year_ = {{ year_ }})
-using (census_tract, name_)
-{% endfor %}
From cea3777bbdf36642b94aad6e182066645c69109b Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 16:11:29 -0400
Subject: [PATCH 111/142] simplify zip_codes
---
dbt/models/all_zip_codes.sql | 20 --------------------
dbt/models/zip_codes.sql | 30 ++++++++++++++++--------------
2 files changed, 16 insertions(+), 34 deletions(-)
delete mode 100644 dbt/models/all_zip_codes.sql
diff --git a/dbt/models/all_zip_codes.sql b/dbt/models/all_zip_codes.sql
deleted file mode 100644
index ac438099..00000000
--- a/dbt/models/all_zip_codes.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-with
-zip_codes as (
-select
- zip_code,
- '[2020-01-01,)'::daterange as valid,
- geom
-from {{ ref('all_zip_codes_2020') }}
-union all
-select
- zip_code,
- '[,2020-01-01)'::daterange as valid,
- geom
-from {{ ref('all_zip_codes_2010') }}
-)
-select
- {{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id
- , zip_code
- , valid
- , geom
-from zip_codes
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 77d9ddd3..e218218a 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -8,19 +8,21 @@
)
}}
-with city_boundary as (
- select
- geom
- from
- {{ ref('city_boundary') }}
+with
+zip_codes as (
+select
+ zip_code,
+ '[2020-01-01,)'::daterange as valid,
+ geom
+from {{ ref('all_zip_codes_2020') }}
+union all
+select
+ zip_code,
+ '[,2020-01-01)'::daterange as valid,
+ geom
+from {{ ref('all_zip_codes_2010') }}
)
select
- all_zip_codes.zip_code_id
- , all_zip_codes.zip_code
- , all_zip_codes.valid
- , all_zip_codes.geom
-from
- {{ ref('all_zip_codes') }} as all_zip_codes,
- city_boundary
-where
- st_intersects(all_zip_codes.geom, city_boundary.geom)
+ {{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id,
+ zip_codes.*
+from zip_codes
From 80e1fb26b1e41ba7857357612acac9dbf4ca65c6 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 16:40:15 -0400
Subject: [PATCH 112/142] document acs data
---
dbt/models/acs_block_group.sql | 11 +++++++++++
dbt/models/acs_tract.sql | 11 +++++++++++
2 files changed, 22 insertions(+)
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index ea77a2b4..a323d31c 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -7,6 +7,17 @@
)
}}
+{% docs acs_block_group %}
+
+Contains American Community Survey (ACS) demographic data at a census block
+group granularity.
+
+The `name_` column contains the name of the demographic variable (e.g.
+`B03002_003E`). See `acs_variables` for a mapping of these codes to
+human-readable names.
+
+{% enddocs %}
+
select
year::smallint as year_,
code as name_,
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index 3a4d1b74..ae113e66 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -7,6 +7,17 @@
)
}}
+{% docs acs_tract %}
+
+Contains American Community Survey (ACS) demographic data at a census tract
+granularity.
+
+The `name_` column contains the name of the demographic variable (e.g.
+`B03002_003E`). See `acs_variables` for a mapping of these codes to
+human-readable names.
+
+{% enddocs %}
+
select
year::smallint as year_,
code as name_,
From 1b4878532ff7f6f0728a8430505f1399c11bbc4c Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 16:40:45 -0400
Subject: [PATCH 113/142] document and reorganize zip code models
---
.../stg_zip_codes_2010.sql} | 0
.../stg_zip_codes_2020.sql} | 0
dbt/models/zip_codes.sql | 11 +++++++++--
3 files changed, 9 insertions(+), 2 deletions(-)
rename dbt/models/{all_zip_codes_2010.sql => staging/stg_zip_codes_2010.sql} (100%)
rename dbt/models/{all_zip_codes_2020.sql => staging/stg_zip_codes_2020.sql} (100%)
diff --git a/dbt/models/all_zip_codes_2010.sql b/dbt/models/staging/stg_zip_codes_2010.sql
similarity index 100%
rename from dbt/models/all_zip_codes_2010.sql
rename to dbt/models/staging/stg_zip_codes_2010.sql
diff --git a/dbt/models/all_zip_codes_2020.sql b/dbt/models/staging/stg_zip_codes_2020.sql
similarity index 100%
rename from dbt/models/all_zip_codes_2020.sql
rename to dbt/models/staging/stg_zip_codes_2020.sql
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index e218218a..623ab23f 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -8,19 +8,26 @@
)
}}
+{% docs zip_codes %}
+
+Contains the geometry and metadata for all zip code tabulation areas (ZCTAs) in
+the United States.
+
+{% enddocs %}
+
with
zip_codes as (
select
zip_code,
'[2020-01-01,)'::daterange as valid,
geom
-from {{ ref('all_zip_codes_2020') }}
+from {{ ref('stg_zip_codes_2020') }}
union all
select
zip_code,
'[,2020-01-01)'::daterange as valid,
geom
-from {{ ref('all_zip_codes_2010') }}
+from {{ ref('stg_zip_codes_2010') }}
)
select
{{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id,
From 489cbaafd2796e7d000b64a902555917d7705a3f Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 16:41:24 -0400
Subject: [PATCH 114/142] document and reorganize residential permit model
---
dbt/models/residential_permits.sql | 46 +++++++++----------
.../residential_permits_to_census_tracts.sql | 31 -------------
.../staging/stg_residential_permits.sql | 25 ++++++++++
.../stg_residential_permits_to_parcels.sql} | 10 ----
4 files changed, 48 insertions(+), 64 deletions(-)
delete mode 100644 dbt/models/residential_permits_to_census_tracts.sql
create mode 100644 dbt/models/staging/stg_residential_permits.sql
rename dbt/models/{residential_permits_to_parcels.sql => staging/stg_residential_permits_to_parcels.sql} (75%)
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 6f994a0a..181af2b5 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -8,28 +8,28 @@
)
}}
+{% docs residential_permits %}
+
+Contains residential building permit applications.
+
+Notes:
+ - Permits are filtered to only include those in Minneapolis.
+ - `square_feet` is treated as missing if it is 0.
+ - `permit_value` is treated as missing if it is 0.
+
+{% enddocs %}
+
+with
+stg_residential_permits as (select * from {{ ref('stg_residential_permits') }}),
+stg_residential_permits_to_parcels as (select * from {{ ref('stg_residential_permits_to_parcels') }}),
+parcels as (select * from {{ ref('parcels') }})
select
- sde_id::int as residential_permit_id
- , year::int as year_
- , tenure::text
- , housing_ty::text as housing_type
- , res_permit::text as permit_type
- , address::text
- , name::text as name_
- , buildings::int as num_buildings
- , units::int as num_units
- , age_restri::int as num_age_restricted_units
- , memory_car::int as num_memory_care_units
- , assisted::int as num_assisted_living_units
- , com_off_re = 'Y' as is_commercial_and_residential
- , nullif(sqf, 0)::int as square_feet
- , public_fun = 'Y' as is_public_funded
- , nullif(permit_val, 0)::int as permit_value
- , community_::text as community_designation
- , notes::text
- , st_transform(geom, {{ var("srid") }}) as geom
+ stg_residential_permits.*,
+ stg_residential_permits_to_parcels.parcel_id,
+ parcels.census_block_group_id,
+ parcels.census_tract_id,
+ parcels.zip_code_id
from
- {{ source('minneapolis', 'residential_permits_residentialpermits') }}
-where
- co_code = '053'
- and lower(ctu_name) = 'minneapolis'
+ stg_residential_permits
+ left join stg_residential_permits_to_parcels using residential_permit_id
+ left join parcels using parcel_id
diff --git a/dbt/models/residential_permits_to_census_tracts.sql b/dbt/models/residential_permits_to_census_tracts.sql
deleted file mode 100644
index 79a48be4..00000000
--- a/dbt/models/residential_permits_to_census_tracts.sql
+++ /dev/null
@@ -1,31 +0,0 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['residential_permit_id']},
- {'columns': ['census_tract_id']}
- ]
- )
-}}
-
-with
-residential_permits as (
- select
- residential_permit_id as id
- , daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
- , geom
- from {{ ref("residential_permits") }}
-)
-, census_tracts as (
- select
- census_tract_id as id
- , valid
- , geom
- from {{ ref("census_tracts") }}
-)
-select
- child_id as residential_permit_id
- , parent_id as census_tract_id
- , valid
- , type_
-from {{ tag_regions("residential_permits", "census_tracts") }}
diff --git a/dbt/models/staging/stg_residential_permits.sql b/dbt/models/staging/stg_residential_permits.sql
new file mode 100644
index 00000000..c6788cc4
--- /dev/null
+++ b/dbt/models/staging/stg_residential_permits.sql
@@ -0,0 +1,25 @@
+select
+ sde_id::int as residential_permit_id
+ , year::smallint as year_
+ , tenure::text
+ , housing_ty::text as housing_type
+ , res_permit::text as permit_type
+ , address::text
+ , name::text as name_
+ , buildings::smallint as num_buildings
+ , units::smallint as num_units
+ , age_restri::smallint as num_age_restricted_units
+ , memory_car::smallint as num_memory_care_units
+ , assisted::smallint as num_assisted_living_units
+ , com_off_re = 'Y' as is_commercial_and_residential
+ , nullif(sqf, 0)::int as square_feet
+ , public_fun = 'Y' as is_public_funded
+ , nullif(permit_val, 0)::int as permit_value
+ , community_::text as community_designation
+ , notes::text
+ , st_transform(geom, {{ var("srid") }}) as geom
+from
+ {{ source('minneapolis', 'residential_permits_residentialpermits') }}
+where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/residential_permits_to_parcels.sql b/dbt/models/staging/stg_residential_permits_to_parcels.sql
similarity index 75%
rename from dbt/models/residential_permits_to_parcels.sql
rename to dbt/models/staging/stg_residential_permits_to_parcels.sql
index daedfab1..2b00cbf0 100644
--- a/dbt/models/residential_permits_to_parcels.sql
+++ b/dbt/models/staging/stg_residential_permits_to_parcels.sql
@@ -1,13 +1,3 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['residential_permit_id']},
- {'columns': ['parcel_id']}
- ]
- )
-}}
-
with
residential_permits as (
select
From d263aae647dce386eeb69e21c18ddce69de70e9a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 16:45:29 -0400
Subject: [PATCH 115/142] reorganize and document commercial permits
---
dbt/models/commercial_permits.sql | 38 +++++++++++--------
dbt/models/staging/stg_commercial_permits.sql | 18 +++++++++
.../stg_commercial_permits_to_parcels.sql} | 10 -----
3 files changed, 40 insertions(+), 26 deletions(-)
create mode 100644 dbt/models/staging/stg_commercial_permits.sql
rename dbt/models/{commercial_permits_to_parcels.sql => staging/stg_commercial_permits_to_parcels.sql} (75%)
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index 4687eb30..2db5d798 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -8,21 +8,27 @@
)
}}
+{% docs commercial_permits %}
+
+Contains commercial building permit applications.
+
+Notes:
+ - Permits are filtered to only include those in Minneapolis.
+ - `square_feet` is treated as missing if it is 0.
+
+{% enddocs %}
+
+with
+stg_commercial_permits as (select * from {{ ref('stg_commercial_permits') }}),
+stg_commercial_permits_to_parcels as (select * from {{ ref('stg_commercial_permits_to_parcels') }}),
+parcels as (select * from {{ ref('parcels') }})
select
- sde_id as commercial_permit_id
- , year::int as year_
- , nonres_gro::text as group_
- , nonres_sub::text as subgroup
- , nonres_typ::text as type_category
- , bldg_name::text as building_name
- , bldg_desc::text as building_description
- , permit_typ::text as permit_type
- , permit_val::int as permit_value
- , nullif(sqf, 0)::int as square_feet
- , address::text
- , st_transform(geom, {{ var("srid") }}) as geom
+ stg_commercial_permits.*,
+ stg_commercial_permits_to_parcels.parcel_id,
+ parcels.census_block_group_id,
+ parcels.census_tract_id,
+ parcels.zip_code_id
from
- {{ source('minneapolis', 'commercial_permits_nonresidentialconstruction') }}
- where
- co_code = '053'
- and lower(ctu_name) = 'minneapolis'
+ stg_commercial_permits
+ left join stg_commercial_permits_to_parcels using commercial_permit_id
+ left join parcels using parcel_id
diff --git a/dbt/models/staging/stg_commercial_permits.sql b/dbt/models/staging/stg_commercial_permits.sql
new file mode 100644
index 00000000..af5aec34
--- /dev/null
+++ b/dbt/models/staging/stg_commercial_permits.sql
@@ -0,0 +1,18 @@
+select
+ sde_id as commercial_permit_id
+ , year::smallint as year_
+ , nonres_gro::text as group_
+ , nonres_sub::text as subgroup
+ , nonres_typ::text as type_category
+ , bldg_name::text as building_name
+ , bldg_desc::text as building_description
+ , permit_typ::text as permit_type
+ , permit_val::int as permit_value
+ , nullif(sqf, 0)::int as square_feet
+ , address::text
+ , st_transform(geom, {{ var("srid") }}) as geom
+from
+ {{ source('minneapolis', 'commercial_permits_nonresidentialconstruction') }}
+ where
+ co_code = '053'
+ and lower(ctu_name) = 'minneapolis'
diff --git a/dbt/models/commercial_permits_to_parcels.sql b/dbt/models/staging/stg_commercial_permits_to_parcels.sql
similarity index 75%
rename from dbt/models/commercial_permits_to_parcels.sql
rename to dbt/models/staging/stg_commercial_permits_to_parcels.sql
index b74a47f4..fc619f42 100644
--- a/dbt/models/commercial_permits_to_parcels.sql
+++ b/dbt/models/staging/stg_commercial_permits_to_parcels.sql
@@ -1,13 +1,3 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['commercial_permit_id']},
- {'columns': ['parcel_id']}
- ]
- )
-}}
-
with
commercial_permits as (
select
From f5ae90c2e0423ed0806e96400229061fd58ac4ce Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 16:48:17 -0400
Subject: [PATCH 116/142] reorganize fair market rents
---
dbt/models/fair_market_rents.sql | 17 ++++++++++++++++-
.../stg_fair_market_rents_union.sql} | 0
2 files changed, 16 insertions(+), 1 deletion(-)
rename dbt/models/{fair_market_rents_union.sql => staging/stg_fair_market_rents_union.sql} (100%)
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index 847d19e1..71fe57e1 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -1,8 +1,23 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['zip_code_id', 'year_', 'num_bedrooms'], 'unique': true}
+ ]
+ )
+}}
+
{% set num_bedrooms = range(0, 5) %}
+{% doc fair_market_rents %}
+
+Contains fair market rent data for different numbers of bedrooms by zip code.
+
+{% enddoc %}
+
with
zip_codes as (select * from {{ ref('zip_codes') }})
-, fair_market_rents as (select * from {{ ref('fair_market_rents_union') }})
+, fair_market_rents as (select * from {{ ref('stg_fair_market_rents_union') }})
, fmr_zip as (
select
zip_codes.zip_code_id
diff --git a/dbt/models/fair_market_rents_union.sql b/dbt/models/staging/stg_fair_market_rents_union.sql
similarity index 100%
rename from dbt/models/fair_market_rents_union.sql
rename to dbt/models/staging/stg_fair_market_rents_union.sql
From 9a84d87007632d9824a2daad743b507260872ee7 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 17:00:14 -0400
Subject: [PATCH 117/142] document high frequency transit lines
---
dbt/models/high_frequency_transit_lines.sql | 12 +++++++++++-
.../stg_high_frequency_transit_lines_union.sql} | 0
2 files changed, 11 insertions(+), 1 deletion(-)
rename dbt/models/{high_frequency_transit_lines_union.sql => staging/stg_high_frequency_transit_lines_union.sql} (100%)
diff --git a/dbt/models/high_frequency_transit_lines.sql b/dbt/models/high_frequency_transit_lines.sql
index 34d1238a..d59af114 100644
--- a/dbt/models/high_frequency_transit_lines.sql
+++ b/dbt/models/high_frequency_transit_lines.sql
@@ -8,7 +8,17 @@
)
}}
-with lines as (select * from {{ ref('high_frequency_transit_lines_union') }})
+{% doc high_frequency_transit_lines %}
+
+Contains the geometry and metadata for high frequency transit lines in the city of Minneapolis.
+
+Notes:
+- `blue_zone_geom` is a 350 foot buffer around both lines and stops.
+- `yellow_zone_geom` is a quarter mile buffer around lines and a half mile buffer around stops.
+
+{% enddoc %}
+
+with lines as (select * from {{ ref('stg_high_frequency_transit_lines_union') }})
, stops as (select * from {{ ref('high_frequency_transit_stops') }})
, lines_and_stops as (
select
diff --git a/dbt/models/high_frequency_transit_lines_union.sql b/dbt/models/staging/stg_high_frequency_transit_lines_union.sql
similarity index 100%
rename from dbt/models/high_frequency_transit_lines_union.sql
rename to dbt/models/staging/stg_high_frequency_transit_lines_union.sql
From ab2eb4346789f0c9ee39031917b7de68c2b0e67a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 17:13:50 -0400
Subject: [PATCH 118/142] reorganize parcels
---
dbt/models/parcels.sql | 20 ++++++++++++++++---
.../stg_parcels.sql} | 10 ----------
.../stg_parcels_to_census_block_groups.sql} | 14 ++-----------
.../stg_parcels_to_zip_codes.sql} | 12 +----------
4 files changed, 20 insertions(+), 36 deletions(-)
rename dbt/models/{parcels_base.sql => staging/stg_parcels.sql} (88%)
rename dbt/models/{parcels_to_census_block_groups.sql => staging/stg_parcels_to_census_block_groups.sql} (56%)
rename dbt/models/{parcels_to_zip_codes.sql => staging/stg_parcels_to_zip_codes.sql} (61%)
diff --git a/dbt/models/parcels.sql b/dbt/models/parcels.sql
index 12a48c54..835262d0 100644
--- a/dbt/models/parcels.sql
+++ b/dbt/models/parcels.sql
@@ -8,10 +8,24 @@
)
}}
+{% doc parcels %}
+
+Contains the geometry and metadata for all parcels in the city of Minneapolis.
+
+Notes:
+- Parcels data is released yearly. Parcels are considered valid for the year they were released.
+- Parcels are filtered to only include those in Minneapolis.
+- `emv_total`, `emv_bldg`, `emv_land`, `year_built`, and `sale_value` are treated as missing if they are 0.
+- `sale_date` is treated as missing if it is equal to `1899-12-30`.
+- `pin` is the county-assigned parcel identification number. The county prefix '053-' is removed.
+- Duplicate rows are removed. Note that this is based on the entire row, not just the `pin`. There may still be duplicate `pin, year_` pairs.
+
+{% enddoc %}
+
with
-parcels as (select * from {{ ref('parcels_base') }}),
-to_zip_codes as (select * from {{ref('parcels_to_zip_codes')}}),
-to_census_bgs as (select * from {{ref('parcels_to_census_block_groups')}}),
+parcels as (select * from {{ ref('stg_parcels') }}),
+to_zip_codes as (select * from {{ref('stg_parcels_to_zip_codes')}}),
+to_census_bgs as (select * from {{ref('stg_parcels_to_census_block_groups')}}),
census_bgs as (select * from {{ref('census_block_groups')}})
select
parcels.*
diff --git a/dbt/models/parcels_base.sql b/dbt/models/staging/stg_parcels.sql
similarity index 88%
rename from dbt/models/parcels_base.sql
rename to dbt/models/staging/stg_parcels.sql
index d4b0a7c9..9ffc665e 100644
--- a/dbt/models/parcels_base.sql
+++ b/dbt/models/staging/stg_parcels.sql
@@ -1,13 +1,3 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['parcel_id'], 'unique': true},
- {'columns': ['valid', 'geom'], 'type': 'gist'}
- ]
- )
-}}
-
{% set years = range(2002, 2024) %}
{% set city = 'MINNEAPOLIS' %}
{% set county_id = '053' %}
diff --git a/dbt/models/parcels_to_census_block_groups.sql b/dbt/models/staging/stg_parcels_to_census_block_groups.sql
similarity index 56%
rename from dbt/models/parcels_to_census_block_groups.sql
rename to dbt/models/staging/stg_parcels_to_census_block_groups.sql
index bb6cc212..39f51d45 100644
--- a/dbt/models/parcels_to_census_block_groups.sql
+++ b/dbt/models/staging/stg_parcels_to_census_block_groups.sql
@@ -1,27 +1,17 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['parcel_id'], 'unique': true},
- {'columns': ['census_block_group_id']}
- ]
- )
-}}
-
with
parcels as (
select
parcel_id as id
, valid
, geom
- from {{ ref("parcels_base") }}
+ from {{ ref('stg_parcels_base') }}
),
census_block_groups as (
select
census_block_group_id as id
, valid
, geom
- from {{ ref("census_block_groups") }}
+ from {{ ref('census_block_groups') }}
)
select
child_id as parcel_id
diff --git a/dbt/models/parcels_to_zip_codes.sql b/dbt/models/staging/stg_parcels_to_zip_codes.sql
similarity index 61%
rename from dbt/models/parcels_to_zip_codes.sql
rename to dbt/models/staging/stg_parcels_to_zip_codes.sql
index 6a045300..964215bb 100644
--- a/dbt/models/parcels_to_zip_codes.sql
+++ b/dbt/models/staging/stg_parcels_to_zip_codes.sql
@@ -1,20 +1,10 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['parcel_id'], 'unique': true},
- {'columns': ['zip_code_id']}
- ]
- )
-}}
-
with
parcels as (
select
parcel_id as id
, valid
, geom
- from {{ ref("parcels_base") }}
+ from {{ ref("stg_parcels_base") }}
),
zip_codes as (
select
From 0bda2c779f17f168b21e1410451d610bb392c752 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Tue, 3 Sep 2024 17:59:21 -0400
Subject: [PATCH 119/142] finish reorganization
---
dbt/models/acs_block_group.sql | 11 --
dbt/models/acs_tract.sql | 11 --
dbt/models/commercial_permits.sql | 10 --
dbt/models/docs.md | 121 ++++++++++++++++++
dbt/models/fair_market_rents.sql | 6 -
dbt/models/high_frequency_transit_lines.sql | 10 --
dbt/models/parcels.sql | 14 --
dbt/models/parking.sql | 50 ++++----
dbt/models/residential_permits.sql | 11 --
dbt/models/schema.yml | 115 +++--------------
dbt/models/segregation_indexes.sql | 7 -
dbt/models/staging/schema.yml | 14 ++
.../stg_commercial_permits_to_parcels.sql | 2 +-
.../stg_parcels_to_census_block_groups.sql | 2 +-
.../staging/stg_parcels_to_zip_codes.sql | 2 +-
dbt/models/staging/stg_parking.sql | 15 +++
.../stg_parking_to_parcels.sql} | 14 +-
.../stg_residential_permits_to_parcels.sql | 2 +-
.../stg_usps_migration_union.sql} | 0
.../tracts_model_int__parcels_filtered.sql | 2 +-
dbt/models/usps_migration.sql | 2 +-
dbt/models/zip_codes.sql | 7 -
22 files changed, 198 insertions(+), 230 deletions(-)
create mode 100644 dbt/models/docs.md
create mode 100644 dbt/models/staging/schema.yml
create mode 100644 dbt/models/staging/stg_parking.sql
rename dbt/models/{parking_to_parcels.sql => staging/stg_parking_to_parcels.sql} (61%)
rename dbt/models/{usps_migration_union.sql => staging/stg_usps_migration_union.sql} (100%)
diff --git a/dbt/models/acs_block_group.sql b/dbt/models/acs_block_group.sql
index a323d31c..ea77a2b4 100644
--- a/dbt/models/acs_block_group.sql
+++ b/dbt/models/acs_block_group.sql
@@ -7,17 +7,6 @@
)
}}
-{% docs acs_block_group %}
-
-Contains American Community Survey (ACS) demographic data at a census block
-group granularity.
-
-The `name_` column contains the name of the demographic variable (e.g.
-`B03002_003E`). See `acs_variables` for a mapping of these codes to
-human-readable names.
-
-{% enddocs %}
-
select
year::smallint as year_,
code as name_,
diff --git a/dbt/models/acs_tract.sql b/dbt/models/acs_tract.sql
index ae113e66..3a4d1b74 100644
--- a/dbt/models/acs_tract.sql
+++ b/dbt/models/acs_tract.sql
@@ -7,17 +7,6 @@
)
}}
-{% docs acs_tract %}
-
-Contains American Community Survey (ACS) demographic data at a census tract
-granularity.
-
-The `name_` column contains the name of the demographic variable (e.g.
-`B03002_003E`). See `acs_variables` for a mapping of these codes to
-human-readable names.
-
-{% enddocs %}
-
select
year::smallint as year_,
code as name_,
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index 2db5d798..58961e23 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -8,16 +8,6 @@
)
}}
-{% docs commercial_permits %}
-
-Contains commercial building permit applications.
-
-Notes:
- - Permits are filtered to only include those in Minneapolis.
- - `square_feet` is treated as missing if it is 0.
-
-{% enddocs %}
-
with
stg_commercial_permits as (select * from {{ ref('stg_commercial_permits') }}),
stg_commercial_permits_to_parcels as (select * from {{ ref('stg_commercial_permits_to_parcels') }}),
diff --git a/dbt/models/docs.md b/dbt/models/docs.md
new file mode 100644
index 00000000..8e0bafd8
--- /dev/null
+++ b/dbt/models/docs.md
@@ -0,0 +1,121 @@
+{% docs commercial_permits %}
+
+Contains commercial building permit applications.
+
+Notes:
+ - Permits are filtered to only include those in Minneapolis.
+ - `square_feet` is treated as missing if it is 0.
+
+{% enddocs %}
+
+{% docs residential_permits %}
+
+Contains residential building permit applications.
+
+Notes:
+ - Permits are filtered to only include those in Minneapolis.
+ - `square_feet` is treated as missing if it is 0.
+ - `permit_value` is treated as missing if it is 0.
+
+{% enddocs %}
+
+{% docs zip_codes %}
+
+Contains the geometry and metadata for all zip code tabulation areas (ZCTAs) in
+the United States.
+
+{% enddocs %}
+
+{% docs parcels %}
+
+Contains the geometry and metadata for all parcels in the city of Minneapolis.
+
+Notes:
+- Parcels data is released yearly. Parcels are considered valid for the year they were released.
+- Parcels are filtered to only include those in Minneapolis.
+- `emv_total`, `emv_bldg`, `emv_land`, `year_built`, and `sale_value` are treated as missing if they are 0.
+- `sale_date` is treated as missing if it is equal to `1899-12-30`.
+- `pin` is the county-assigned parcel identification number. The county prefix '053-' is removed.
+- Duplicate rows are removed. Note that this is based on the entire row, not just the `pin`. There may still be duplicate `pin, year_` pairs.
+
+{% enddocs %}
+
+{% docs census_tracts %}
+
+Contains geometry and metadata for census tracts. Currently only includes census
+tracts for Minnesota.
+
+{% enddocs %}
+
+{% docs census_block_groups %}
+
+Contains geometry and metadata for census block groups. Currently only includes
+census block groups for Minnesota.
+
+{% enddocs %}
+
+{% docs acs_block_group %}
+
+Contains American Community Survey (ACS) demographic data at a census block
+group granularity.
+
+The `name_` column contains the name of the demographic variable (e.g.
+`B03002_003E`). See `acs_variables` for a mapping of these codes to
+human-readable names.
+
+{% enddocs %}
+
+{% docs acs_tract %}
+
+Contains American Community Survey (ACS) demographic data at a census tract
+granularity.
+
+The `name_` column contains the name of the demographic variable (e.g.
+`B03002_003E`). See `acs_variables` for a mapping of these codes to
+human-readable names.
+
+{% enddocs %}
+
+{% docs fair_market_rents %}
+
+Contains fair market rent data for different numbers of bedrooms by zip code.
+
+{% enddocs %}
+
+{% docs high_frequency_transit_lines %}
+
+Contains the geometry and metadata for high frequency transit lines in the city of Minneapolis.
+
+Notes:
+- `blue_zone_geom` is a 350 foot buffer around both lines and stops.
+- `yellow_zone_geom` is a quarter mile buffer around lines and a half mile buffer around stops.
+
+{% enddocs %}
+
+{% docs segregation_indexes %}
+
+Segregation index for each tract for each year, computed for each reference
+distribution.
+
+The segregation index is the KL-divergence between the distribution of
+population in a tract and a reference distribution. For example, a tract that
+has many more white people than the average for the city will have a high
+segregation index for the 'average_city' distribution.
+
+Available distributions:
+- `uniform`: Uniform distribution.
+- `annual_city`: Citywide distribution for the current year.
+- `average_city`: Citywide distribution averaged over all available years.
+
+{% enddocs %}
+
+{% docs usps_migration %}
+
+Contains USPS migration data sourced from change of address forms. Migrations
+are broken down by month and year, zip_code, flow direction, and flow type. Flow
+directions are either `from` (out of) the zip code or `to` (in to) the zip code.
+
+Flow types are one of `business`, `family`, `individual`, `perm` (permanent),
+`temp` (temporary), or `total`.
+
+{% enddocs %}
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index 71fe57e1..92c9bafc 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -9,12 +9,6 @@
{% set num_bedrooms = range(0, 5) %}
-{% doc fair_market_rents %}
-
-Contains fair market rent data for different numbers of bedrooms by zip code.
-
-{% enddoc %}
-
with
zip_codes as (select * from {{ ref('zip_codes') }})
, fair_market_rents as (select * from {{ ref('stg_fair_market_rents_union') }})
diff --git a/dbt/models/high_frequency_transit_lines.sql b/dbt/models/high_frequency_transit_lines.sql
index d59af114..c27885ca 100644
--- a/dbt/models/high_frequency_transit_lines.sql
+++ b/dbt/models/high_frequency_transit_lines.sql
@@ -8,16 +8,6 @@
)
}}
-{% doc high_frequency_transit_lines %}
-
-Contains the geometry and metadata for high frequency transit lines in the city of Minneapolis.
-
-Notes:
-- `blue_zone_geom` is a 350 foot buffer around both lines and stops.
-- `yellow_zone_geom` is a quarter mile buffer around lines and a half mile buffer around stops.
-
-{% enddoc %}
-
with lines as (select * from {{ ref('stg_high_frequency_transit_lines_union') }})
, stops as (select * from {{ ref('high_frequency_transit_stops') }})
, lines_and_stops as (
diff --git a/dbt/models/parcels.sql b/dbt/models/parcels.sql
index 835262d0..bd7b07ab 100644
--- a/dbt/models/parcels.sql
+++ b/dbt/models/parcels.sql
@@ -8,20 +8,6 @@
)
}}
-{% doc parcels %}
-
-Contains the geometry and metadata for all parcels in the city of Minneapolis.
-
-Notes:
-- Parcels data is released yearly. Parcels are considered valid for the year they were released.
-- Parcels are filtered to only include those in Minneapolis.
-- `emv_total`, `emv_bldg`, `emv_land`, `year_built`, and `sale_value` are treated as missing if they are 0.
-- `sale_date` is treated as missing if it is equal to `1899-12-30`.
-- `pin` is the county-assigned parcel identification number. The county prefix '053-' is removed.
-- Duplicate rows are removed. Note that this is based on the entire row, not just the `pin`. There may still be duplicate `pin, year_` pairs.
-
-{% enddoc %}
-
with
parcels as (select * from {{ ref('stg_parcels') }}),
to_zip_codes as (select * from {{ref('stg_parcels_to_zip_codes')}}),
diff --git a/dbt/models/parking.sql b/dbt/models/parking.sql
index ac31de4a..e49574fe 100644
--- a/dbt/models/parking.sql
+++ b/dbt/models/parking.sql
@@ -1,30 +1,24 @@
-with
- parking_raw as (
- select
- ogc_fid
- , "date"
- , "project na"
- , address
- , neighborho
- , ward
- , "downtown y"
- , "housing un"
- , "car parkin"
- , "bike parki"
- , "year"
- , geom
- from {{ source('minneapolis', 'parking_parcels') }}
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parking_id'], 'unique': true},
+ {'columns': ['geom'], 'type': 'gist'}
+ ]
)
+}}
+
+with
+ stg_parking as (select * from {{ ref('stg_parking') }}),
+ stg_parking_to_parcels as (select * from {{ ref('stg_parking_to_parcels') }}),
+ parcels as (select * from {{ ref('parcels') }})
select
- ogc_fid as parking_id
- , to_date("year" || '-' || "date", 'YYYY-DD-Mon') as date_
- , "project na"::text as project_name
- , address::text
- , neighborho::text as neighborhood
- , ward::int
- , "downtown y" = 'Y' as is_downtown
- , "housing un"::int as num_housing_units
- , "car parkin"::int as num_car_parking_spaces
- , "bike parki"::int as num_bike_parking_spaces
- , st_transform(geom, {{ var("srid") }}) as geom
-from parking_raw
+ stg_parking.*,
+ stg_parking_to_parcels.parcel_id,
+ parcels.census_block_group_id,
+ parcels.census_tract_id,
+ parcels.zip_code_id
+from
+ stg_parking
+ left join stg_parking_to_parcels using parking_id
+ left join parcels using parcel_id
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index 181af2b5..bcba6ab4 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -8,17 +8,6 @@
)
}}
-{% docs residential_permits %}
-
-Contains residential building permit applications.
-
-Notes:
- - Permits are filtered to only include those in Minneapolis.
- - `square_feet` is treated as missing if it is 0.
- - `permit_value` is treated as missing if it is 0.
-
-{% enddocs %}
-
with
stg_residential_permits as (select * from {{ ref('stg_residential_permits') }}),
stg_residential_permits_to_parcels as (select * from {{ ref('stg_residential_permits_to_parcels') }}),
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 2ed844c8..5022e8ea 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -90,6 +90,7 @@ sources:
models:
- name: census_tracts
+ description: '{{ doc("census_tracts") }}'
columns:
- name: census_tract_id
data_tests:
@@ -97,6 +98,7 @@ models:
- not_null
- name: census_block_groups
+ description: '{{ doc("census_block_groups") }}'
columns:
- name: census_block_group_id
data_tests:
@@ -109,12 +111,7 @@ models:
field: census_tract_id
- name: acs_block_group
- data_tests:
- - dbt_utils.unique_combination_of_columns:
- combination_of_columns:
- - census_block_group_id
- - year_
- - name_
+ description: '{{ doc("acs_block_group") }}'
columns:
- name: census_block_group_id
data_tests:
@@ -122,7 +119,17 @@ models:
to: ref('census_block_groups')
field: census_block_group_id
+ - name: acs_tract
+ description: '{{ doc("acs_tract") }}'
+
+ - name: fair_market_rents
+ description: '{{ doc("fair_market_rents") }}'
+
+ - name: high_frequency_transit_lines
+ description: '{{ doc("high_frequency_transit_lines") }}'
+
- name: segregation_indexes
+ description: '{{ doc("segregation_indexes") }}'
data_tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
@@ -137,6 +144,7 @@ models:
field: census_tract
- name: parcels
+ description: '{{ doc("parcels") }}'
columns:
- name: parcel_id
data_tests:
@@ -154,68 +162,8 @@ models:
to: ref('census_block_groups')
field: census_block_group_id
- - name: parcels_to_census_block_groups
- data_tests:
- - dbt_utils.unique_combination_of_columns:
- combination_of_columns:
- - parcel_id
- - census_block_group_id
- columns:
- - name: parcel_id
- data_tests:
- - not_null
- - relationships:
- to: ref('parcels')
- field: parcel_id
- - name: census_block_group_id
- data_tests:
- - not_null
- - relationships:
- to: ref('census_block_groups')
- field: census_block_group_id
-
- - name: parcels_to_zip_codes
- data_tests:
- - dbt_utils.unique_combination_of_columns:
- combination_of_columns:
- - parcel_id
- - zip_code_id
- columns:
- - name: parcel_id
- data_tests:
- - not_null
- - relationships:
- to: ref('parcels')
- field: parcel_id
- - name: zip_code_id
- data_tests:
- - not_null
- - relationships:
- to: ref('zip_codes')
- field: zip_code_id
-
- - name: all_zip_codes_2010
- columns:
- - name: zip_code
- data_tests:
- - not_null
- - unique
-
- - name: all_zip_codes_2020
- columns:
- - name: zip_code
- data_tests:
- - not_null
- - unique
-
- - name: all_zip_codes
- columns:
- - name: zip_code_id
- data_tests:
- - not_null
- - unique
-
- name: zip_codes
+ description: '{{ doc("zip_codes") }}'
columns:
- name: zip_code_id
data_tests:
@@ -223,6 +171,7 @@ models:
- unique
- name: usps_migration
+ description: '{{ doc("usps_migration") }}'
data_tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
@@ -239,6 +188,7 @@ models:
field: zip_code_id
- name: commercial_permits
+ description: '{{ doc("commercial_permits") }}'
columns:
- name: commercial_permit_id
data_tests:
@@ -246,42 +196,13 @@ models:
- unique
- name: residential_permits
+ description: '{{ doc("residential_permits") }}'
columns:
- name: residential_permit_id
data_tests:
- not_null
- unique
- - name: residential_permits_to_parcels
- columns:
- - name: residential_permit_id
- data_tests:
- - not_null
- - relationships:
- to: ref('residential_permits')
- field: residential_permit_id
- - name: parcel_id
- data_tests:
- - not_null
- - relationships:
- to: ref('parcels')
- field: parcel_id
-
- - name: commercial_permits_to_parcels
- columns:
- - name: commercial_permit_id
- data_tests:
- - not_null
- - relationships:
- to: ref('commercial_permits')
- field: commercial_permit_id
- - name: parcel_id
- data_tests:
- - not_null
- - relationships:
- to: ref('parcels')
- field: parcel_id
-
- name: neighborhoods
columns:
- name: neighborhood_id
diff --git a/dbt/models/segregation_indexes.sql b/dbt/models/segregation_indexes.sql
index 90f48b7b..cdadbc67 100644
--- a/dbt/models/segregation_indexes.sql
+++ b/dbt/models/segregation_indexes.sql
@@ -7,13 +7,6 @@
)
}}
--- Segregation index for each tract for each year, computed for each reference
--- distribution.
---
--- The segregation index is the KL-divergence between the distribution of
--- population in a tract and a reference distribution. For example, a tract that
--- has many more white people than the average for the city will have a high
--- segregation index for the 'average_city' distribution.
with
categories as (select * from {{ ref("population_categories") }})
, acs_tract_all as (select * from {{ ref("acs_tract") }})
diff --git a/dbt/models/staging/schema.yml b/dbt/models/staging/schema.yml
new file mode 100644
index 00000000..5328c7d1
--- /dev/null
+++ b/dbt/models/staging/schema.yml
@@ -0,0 +1,14 @@
+models:
+ - name: stg_zip_codes_2010
+ columns:
+ - name: zip_code
+ data_tests:
+ - not_null
+ - unique
+
+ - name: stg_zip_codes_2020
+ columns:
+ - name: zip_code
+ data_tests:
+ - not_null
+ - unique
diff --git a/dbt/models/staging/stg_commercial_permits_to_parcels.sql b/dbt/models/staging/stg_commercial_permits_to_parcels.sql
index fc619f42..bbc44326 100644
--- a/dbt/models/staging/stg_commercial_permits_to_parcels.sql
+++ b/dbt/models/staging/stg_commercial_permits_to_parcels.sql
@@ -4,7 +4,7 @@ commercial_permits as (
commercial_permit_id as id
, daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
, geom
- from {{ ref("commercial_permits") }}
+ from {{ ref('stg_commercial_permits') }}
)
, parcels as (
select
diff --git a/dbt/models/staging/stg_parcels_to_census_block_groups.sql b/dbt/models/staging/stg_parcels_to_census_block_groups.sql
index 39f51d45..d65f230f 100644
--- a/dbt/models/staging/stg_parcels_to_census_block_groups.sql
+++ b/dbt/models/staging/stg_parcels_to_census_block_groups.sql
@@ -4,7 +4,7 @@ parcels as (
parcel_id as id
, valid
, geom
- from {{ ref('stg_parcels_base') }}
+ from {{ ref('stg_parcels') }}
),
census_block_groups as (
select
diff --git a/dbt/models/staging/stg_parcels_to_zip_codes.sql b/dbt/models/staging/stg_parcels_to_zip_codes.sql
index 964215bb..15b643c7 100644
--- a/dbt/models/staging/stg_parcels_to_zip_codes.sql
+++ b/dbt/models/staging/stg_parcels_to_zip_codes.sql
@@ -4,7 +4,7 @@ parcels as (
parcel_id as id
, valid
, geom
- from {{ ref("stg_parcels_base") }}
+ from {{ ref("stg_parcels") }}
),
zip_codes as (
select
diff --git a/dbt/models/staging/stg_parking.sql b/dbt/models/staging/stg_parking.sql
new file mode 100644
index 00000000..ed00a8b1
--- /dev/null
+++ b/dbt/models/staging/stg_parking.sql
@@ -0,0 +1,15 @@
+with
+parking_raw as (select * from {{ source('minneapolis', 'parking_parcels') }})
+select
+ ogc_fid as parking_id
+ , to_date("year" || '-' || "date", 'YYYY-DD-Mon') as date_
+ , "project na"::text as project_name
+ , address::text
+ , neighborho::text as neighborhood
+ , ward::smallint
+ , "downtown y" = 'Y' as is_downtown
+ , "housing un"::smallint as num_housing_units
+ , "car parkin"::smallint as num_car_parking_spaces
+ , "bike parki"::smallint as num_bike_parking_spaces
+ , st_transform(geom, {{ var("srid") }}) as geom
+from parking_raw
diff --git a/dbt/models/parking_to_parcels.sql b/dbt/models/staging/stg_parking_to_parcels.sql
similarity index 61%
rename from dbt/models/parking_to_parcels.sql
rename to dbt/models/staging/stg_parking_to_parcels.sql
index 7eb1c755..6e708e17 100644
--- a/dbt/models/parking_to_parcels.sql
+++ b/dbt/models/staging/stg_parking_to_parcels.sql
@@ -1,27 +1,17 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['parking_id']},
- {'columns': ['parcel_id']}
- ]
- )
-}}
-
with
parking as (
select
parking_id as id
, daterange(date_, date_, '[]') as valid
, geom
- from {{ ref('parking') }}
+ from {{ ref('stg_parking') }}
)
, parcels as (
select
parcel_id as id
, valid
, geom
- from {{ ref('parcels_base') }}
+ from {{ ref('parcels') }}
)
select
child_id as parking_id
diff --git a/dbt/models/staging/stg_residential_permits_to_parcels.sql b/dbt/models/staging/stg_residential_permits_to_parcels.sql
index 2b00cbf0..d3b5ae37 100644
--- a/dbt/models/staging/stg_residential_permits_to_parcels.sql
+++ b/dbt/models/staging/stg_residential_permits_to_parcels.sql
@@ -4,7 +4,7 @@ residential_permits as (
residential_permit_id as id
, daterange(to_date(year_::text, 'YYYY'), to_date(year_::text, 'YYYY'), '[]') as valid
, geom
- from {{ ref("residential_permits") }}
+ from {{ ref('stg_residential_permits') }}
)
, parcels as (
select
diff --git a/dbt/models/usps_migration_union.sql b/dbt/models/staging/stg_usps_migration_union.sql
similarity index 100%
rename from dbt/models/usps_migration_union.sql
rename to dbt/models/staging/stg_usps_migration_union.sql
diff --git a/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
index f14ca0fc..80055cc2 100644
--- a/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
+++ b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
@@ -7,7 +7,7 @@
-- Retag parcels with census tracts (because we replaced the 2020 tracts with the 2019 tracts)
with
census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }}),
-parcels as (select * from {{ ref('parcels_base') }}),
+parcels as (select * from {{ ref('parcels') }}),
parcels_tag as (select parcel_id as id, valid, geom from parcels),
census_tracts_tag as (select census_tract_id as id, valid, geom from census_tracts),
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 541b32c1..0bb0ef93 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -14,7 +14,7 @@ with
zip_codes as (select * from {{ ref('zip_codes') }})
, process_date as (
select to_date(yyyy_mm, 'YYYYMM') as date_, *
- from {{ ref('usps_migration_union') }}
+ from {{ ref('stg_usps_migration_union') }}
)
, add_zip_id as (
select zip_code_id, process_date.*
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
index 623ab23f..078f14ef 100644
--- a/dbt/models/zip_codes.sql
+++ b/dbt/models/zip_codes.sql
@@ -8,13 +8,6 @@
)
}}
-{% docs zip_codes %}
-
-Contains the geometry and metadata for all zip code tabulation areas (ZCTAs) in
-the United States.
-
-{% enddocs %}
-
with
zip_codes as (
select
From fd89ca43ebbf9153c38b42c119ef92b4d3d4c3eb Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 11:05:12 -0400
Subject: [PATCH 120/142] reorganize fair market rents data
---
dbt/models/fair_market_rents.sql | 39 +++++++------------
dbt/models/schema.yml | 6 ---
.../staging/stg_fair_market_rents_dedup.sql | 1 +
.../staging/stg_fair_market_rents_union.sql | 10 ++---
.../staging/stg_fair_market_rents_unpivot.sql | 16 ++++++++
5 files changed, 36 insertions(+), 36 deletions(-)
create mode 100644 dbt/models/staging/stg_fair_market_rents_dedup.sql
create mode 100644 dbt/models/staging/stg_fair_market_rents_unpivot.sql
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index 92c9bafc..40979aa7 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -2,35 +2,24 @@
config(
materialized='table',
indexes = [
- {'columns': ['zip_code_id', 'year_', 'num_bedrooms'], 'unique': true}
+ {'columns': ['zip_code_id', 'year_', 'num_bedrooms']}
]
)
}}
-{% set num_bedrooms = range(0, 5) %}
-
with
+stg_fair_market_rents_unpivot as (
+ select * from {{ ref('stg_fair_market_rents_unpivot') }}
+),
zip_codes as (select * from {{ ref('zip_codes') }})
-, fair_market_rents as (select * from {{ ref('stg_fair_market_rents_union') }})
-, fmr_zip as (
- select
- zip_codes.zip_code_id
- {% for bedroom in num_bedrooms %}
- , fair_market_rents.rent_br{{ bedroom }}
- {% endfor %}
- , fair_market_rents.year_
- from
- fair_market_rents
- inner join zip_codes
- on zip_codes.zip_code = fair_market_rents.zip_code
- and zip_codes.valid @> to_date(year_::text , 'YYYY')
-)
-{% for bedroom in num_bedrooms %}
select
- zip_code_id
- , rent_br{{ bedroom }}::int as rent
- , {{ bedroom }} as num_bedrooms
- , year_::int
-from fmr_zip
-{% if not loop.last %} union all {% endif %}
-{% endfor %}
+ zip_codes.zip_code_id,
+ stg_fair_market_rents_unpivot.zip_code,
+ stg_fair_market_rents_unpivot.year_::smallint,
+ stg_fair_market_rents_unpivot.num_bedrooms::smallint,
+ stg_fair_market_rents_unpivot.rent::smallint
+from
+ stg_fair_market_rents_unpivot
+ left join zip_codes
+ on stg_fair_market_rents_unpivot.zip_code = zip_codes.zip_code
+ and (stg_fair_market_rents_unpivot.year_ || '-01-01')::date <@ zip_codes.valid
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 5022e8ea..b2b0ea78 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -112,12 +112,6 @@ models:
- name: acs_block_group
description: '{{ doc("acs_block_group") }}'
- columns:
- - name: census_block_group_id
- data_tests:
- - relationships:
- to: ref('census_block_groups')
- field: census_block_group_id
- name: acs_tract
description: '{{ doc("acs_tract") }}'
diff --git a/dbt/models/staging/stg_fair_market_rents_dedup.sql b/dbt/models/staging/stg_fair_market_rents_dedup.sql
new file mode 100644
index 00000000..fec86c06
--- /dev/null
+++ b/dbt/models/staging/stg_fair_market_rents_dedup.sql
@@ -0,0 +1 @@
+select distinct * from {{ ref('stg_fair_market_rents_unpivot') }}
diff --git a/dbt/models/staging/stg_fair_market_rents_union.sql b/dbt/models/staging/stg_fair_market_rents_union.sql
index 696d0a34..5bf52020 100644
--- a/dbt/models/staging/stg_fair_market_rents_union.sql
+++ b/dbt/models/staging/stg_fair_market_rents_union.sql
@@ -3,11 +3,11 @@
{% for year_ in years %}
select
zip_code
- , rent_br0
- , rent_br1
- , rent_br2
- , rent_br3
- , rent_br4
+ , replace(rent_br0, '.00', '') as rent_br0
+ , replace(rent_br1, '.00', '') as rent_br1
+ , replace(rent_br2, '.00', '') as rent_br2
+ , replace(rent_br3, '.00', '') as rent_br3
+ , replace(rent_br4, '.00', '') as rent_br4
, year as year_
from
{{ source('minneapolis', 'fair_market_rents_' ~ year_) }}
diff --git a/dbt/models/staging/stg_fair_market_rents_unpivot.sql b/dbt/models/staging/stg_fair_market_rents_unpivot.sql
new file mode 100644
index 00000000..92e64612
--- /dev/null
+++ b/dbt/models/staging/stg_fair_market_rents_unpivot.sql
@@ -0,0 +1,16 @@
+with
+stg_fair_market_rents_dedup as (select * from {{ ref('stg_fair_market_rents_union') }})
+select
+ stg_fair_market_rents_dedup.zip_code,
+ stg_fair_market_rents_dedup.year_,
+ x.num_bedrooms,
+ x.rent
+from
+ stg_fair_market_rents_dedup
+ cross join lateral (
+ values (0, rent_br0),
+ (1, rent_br1),
+ (2, rent_br2),
+ (3, rent_br3),
+ (4, rent_br4)
+ ) as x(num_bedrooms, rent)
From 4d3cf74b564f7c66917112ced359eea93d10ac22 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 11:40:21 -0400
Subject: [PATCH 121/142] add mapping table for translating from zip codes to
zctas
---
dbt/models/docs.md | 6 ++-
dbt/models/fair_market_rents.sql | 14 +++---
dbt/models/parcels.sql | 4 +-
dbt/models/schema.yml | 21 +++++----
dbt/models/staging/schema.yml | 8 ++--
...zip_codes.sql => stg_parcels_to_zctas.sql} | 10 ++--
.../staging/stg_usps_migration_unpivot.sql | 29 ++++++++++++
dbt/models/staging/stg_zctas_2010.sql | 4 ++
..._zip_codes_2020.sql => stg_zctas_2020.sql} | 2 +-
dbt/models/staging/stg_zip_codes_2010.sql | 5 --
dbt/models/usps_migration.sql | 46 +++++--------------
dbt/models/zctas.sql | 28 +++++++++++
dbt/models/zip_codes.sql | 28 -----------
dbt/models/zip_codes_to_zctas.sql | 2 +
14 files changed, 111 insertions(+), 96 deletions(-)
rename dbt/models/staging/{stg_parcels_to_zip_codes.sql => stg_parcels_to_zctas.sql} (57%)
create mode 100644 dbt/models/staging/stg_usps_migration_unpivot.sql
create mode 100644 dbt/models/staging/stg_zctas_2010.sql
rename dbt/models/staging/{stg_zip_codes_2020.sql => stg_zctas_2020.sql} (82%)
delete mode 100644 dbt/models/staging/stg_zip_codes_2010.sql
create mode 100644 dbt/models/zctas.sql
delete mode 100644 dbt/models/zip_codes.sql
create mode 100644 dbt/models/zip_codes_to_zctas.sql
diff --git a/dbt/models/docs.md b/dbt/models/docs.md
index 8e0bafd8..02494f2e 100644
--- a/dbt/models/docs.md
+++ b/dbt/models/docs.md
@@ -19,11 +19,15 @@ Notes:
{% enddocs %}
-{% docs zip_codes %}
+{% docs zctas %}
Contains the geometry and metadata for all zip code tabulation areas (ZCTAs) in
the United States.
+These are not the same as zip codes. Zip codes are created by the postal service, and they change regularly. ZCTAs are created by the census bureau alongside the census. Not every zip code has a corresponding ZCTA (unpopulated zip codes are not represented, for example), and some ZCTAs cover multiple zip codes.
+
+Use the mapping table `zip_codes_to_zctas` to translate from zip codes to ZCTAs.
+
{% enddocs %}
{% docs parcels %}
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index 40979aa7..c82afc4b 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -11,15 +11,17 @@ with
stg_fair_market_rents_unpivot as (
select * from {{ ref('stg_fair_market_rents_unpivot') }}
),
-zip_codes as (select * from {{ ref('zip_codes') }})
+zip_codes_to_zctas as (select * from {{ ref('zip_codes_to_zctas') }}),
+zctas as (select * from {{ ref('zctas') }})
select
- zip_codes.zip_code_id,
stg_fair_market_rents_unpivot.zip_code,
stg_fair_market_rents_unpivot.year_::smallint,
stg_fair_market_rents_unpivot.num_bedrooms::smallint,
- stg_fair_market_rents_unpivot.rent::smallint
+ stg_fair_market_rents_unpivot.rent::smallint,
+ zctas.zcta_id
from
stg_fair_market_rents_unpivot
- left join zip_codes
- on stg_fair_market_rents_unpivot.zip_code = zip_codes.zip_code
- and (stg_fair_market_rents_unpivot.year_ || '-01-01')::date <@ zip_codes.valid
+ left join zip_codes_to_zctas using zip_code
+ left join zctas
+ on zip_codes_to_zctas.zcta = zctas.zcta
+ and (stg_fair_market_rents_unpivot.year_ || '-01-01')::date <@ zctas.valid
diff --git a/dbt/models/parcels.sql b/dbt/models/parcels.sql
index bd7b07ab..9897974f 100644
--- a/dbt/models/parcels.sql
+++ b/dbt/models/parcels.sql
@@ -10,12 +10,12 @@
with
parcels as (select * from {{ ref('stg_parcels') }}),
-to_zip_codes as (select * from {{ref('stg_parcels_to_zip_codes')}}),
+to_zctas as (select * from {{ref('stg_parcels_to_zctas')}}),
to_census_bgs as (select * from {{ref('stg_parcels_to_census_block_groups')}}),
census_bgs as (select * from {{ref('census_block_groups')}})
select
parcels.*
- , to_zip_codes.zip_code_id
+ , to_zctas.zcta_id
, to_census_bgs.census_block_group_id
, census_bgs.census_tract_id
from
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index b2b0ea78..52800c91 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -37,6 +37,7 @@ sources:
- name: usps_y2023
- name: zip_codes_tl_2020_us_zcta510
- name: zip_codes_tl_2020_us_zcta520
+ - name: zip_codes_zcta_xref
- name: census_cb_2010_27_bg_500k
- name: census_cb_2010_27_tract_500k
- name: census_cb_2013_27_bg_500k
@@ -144,22 +145,22 @@ models:
data_tests:
- unique
- not_null
- - name: zip_code_id
+ - name: zcta_id
data_tests:
- not_null
- relationships:
- to: ref('zip_codes')
- field: zip_code_id
+ to: ref('zctas')
+ field: zcta_id
- name: census_block_group_id
data_tests:
- relationships:
to: ref('census_block_groups')
field: census_block_group_id
- - name: zip_codes
- description: '{{ doc("zip_codes") }}'
+ - name: zctas
+ description: '{{ doc("zctas") }}'
columns:
- - name: zip_code_id
+ - name: zcta_id
data_tests:
- not_null
- unique
@@ -170,16 +171,16 @@ models:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- date_
- - zip_code_id
+ - zcta_id
- flow_direction
- flow_type
columns:
- - name: zip_code_id
+ - name: zcta_id
data_tests:
- not_null
- relationships:
- to: ref('zip_codes')
- field: zip_code_id
+ to: ref('zctas')
+ field: zcta_id
- name: commercial_permits
description: '{{ doc("commercial_permits") }}'
diff --git a/dbt/models/staging/schema.yml b/dbt/models/staging/schema.yml
index 5328c7d1..dccd58b5 100644
--- a/dbt/models/staging/schema.yml
+++ b/dbt/models/staging/schema.yml
@@ -1,14 +1,14 @@
models:
- - name: stg_zip_codes_2010
+ - name: stg_zctas_2010
columns:
- - name: zip_code
+ - name: zcta
data_tests:
- not_null
- unique
- - name: stg_zip_codes_2020
+ - name: stg_zctas_2020
columns:
- - name: zip_code
+ - name: zcta
data_tests:
- not_null
- unique
diff --git a/dbt/models/staging/stg_parcels_to_zip_codes.sql b/dbt/models/staging/stg_parcels_to_zctas.sql
similarity index 57%
rename from dbt/models/staging/stg_parcels_to_zip_codes.sql
rename to dbt/models/staging/stg_parcels_to_zctas.sql
index 15b643c7..680e304e 100644
--- a/dbt/models/staging/stg_parcels_to_zip_codes.sql
+++ b/dbt/models/staging/stg_parcels_to_zctas.sql
@@ -6,16 +6,16 @@ parcels as (
, geom
from {{ ref("stg_parcels") }}
),
-zip_codes as (
+zctas as (
select
- zip_code_id as id
+ zcta_id as id
, valid
, geom
- from {{ ref("zip_codes") }}
+ from {{ ref("zctas") }}
)
select
child_id as parcel_id
- , parent_id as zip_code_id
+ , parent_id as zcta_id
, valid
, type_
-from {{ tag_regions("parcels", "zip_codes") }}
+from {{ tag_regions("parcels", "zctas") }}
diff --git a/dbt/models/staging/stg_usps_migration_unpivot.sql b/dbt/models/staging/stg_usps_migration_unpivot.sql
new file mode 100644
index 00000000..d8ba1c49
--- /dev/null
+++ b/dbt/models/staging/stg_usps_migration_unpivot.sql
@@ -0,0 +1,29 @@
+{% set usps_migration_flow_types = ['business', 'family', 'individual', 'perm', 'temp'] %}
+{% set usps_migration_flow_directions = ['from', 'to'] %}
+
+with
+process_date as (
+ select to_date(yyyy_mm, 'YYYYMM') as date_, *
+ from {{ ref('stg_usps_migration_union') }}
+)
+{% for flow_direction in usps_migration_flow_directions %}
+ select
+ date_
+ , zip_code
+ , '{{ flow_direction }}' as flow_direction
+ , 'total' as flow_type
+ , total_{{ flow_direction }}_zip::int as flow_value
+ from process_date
+ union all
+ {% for flow_type in usps_migration_flow_types %}
+ select
+ date_
+ , zip_code
+ , '{{ flow_direction }}' as flow_direction
+ , '{{ flow_type }}' as flow_type
+ , total_{{ flow_direction }}_zip_{{ flow_type }}::int as flow_value
+ from process_date
+ {% if not loop.last %} union all {% endif %}
+ {% endfor %}
+{% if not loop.last %} union all {% endif %}
+{% endfor %}
diff --git a/dbt/models/staging/stg_zctas_2010.sql b/dbt/models/staging/stg_zctas_2010.sql
new file mode 100644
index 00000000..51921be6
--- /dev/null
+++ b/dbt/models/staging/stg_zctas_2010.sql
@@ -0,0 +1,4 @@
+select
+ zcta5ce10 as zcta,
+ st_transform(geom, {{ var("srid") }}) as geom
+from {{ source('minneapolis', 'zip_codes_tl_2020_us_zcta510') }}
diff --git a/dbt/models/staging/stg_zip_codes_2020.sql b/dbt/models/staging/stg_zctas_2020.sql
similarity index 82%
rename from dbt/models/staging/stg_zip_codes_2020.sql
rename to dbt/models/staging/stg_zctas_2020.sql
index 9a9a77b0..21c131d1 100644
--- a/dbt/models/staging/stg_zip_codes_2020.sql
+++ b/dbt/models/staging/stg_zctas_2020.sql
@@ -1,4 +1,4 @@
select
- zcta5ce20 as zip_code,
+ zcta5ce20 as zcta,
st_transform(geom, {{ var("srid") }}) as geom
from {{ source('minneapolis', 'zip_codes_tl_2020_us_zcta520') }}
diff --git a/dbt/models/staging/stg_zip_codes_2010.sql b/dbt/models/staging/stg_zip_codes_2010.sql
deleted file mode 100644
index e6f2c5c5..00000000
--- a/dbt/models/staging/stg_zip_codes_2010.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-select
- zcta5ce10 as zip_code,
- st_transform(geom, {{ var("srid") }}) as geom
-from
- {{ source('minneapolis', 'zip_codes_tl_2020_us_zcta510') }}
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index 0bb0ef93..ed028cdc 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -11,37 +11,15 @@
{% set usps_migration_flow_directions = ['from', 'to'] %}
with
-zip_codes as (select * from {{ ref('zip_codes') }})
-, process_date as (
- select to_date(yyyy_mm, 'YYYYMM') as date_, *
- from {{ ref('stg_usps_migration_union') }}
-)
-, add_zip_id as (
- select zip_code_id, process_date.*
- from
- process_date
- inner join zip_codes
- on zip_codes.zip_code = replace(process_date.zip_code, '=', '')
- and process_date.date_ <@ zip_codes.valid
-)
-{% for flow_direction in usps_migration_flow_directions %}
- select
- date_
- , zip_code_id
- , '{{ flow_direction }}' as flow_direction
- , 'total' as flow_type
- , total_{{ flow_direction }}_zip::int as flow_value
- from add_zip_id
- union all
- {% for flow_type in usps_migration_flow_types %}
- select
- date_
- , zip_code_id
- , '{{ flow_direction }}' as flow_direction
- , '{{ flow_type }}' as flow_type
- , total_{{ flow_direction }}_zip_{{ flow_type }}::int as flow_value
- from add_zip_id
- {% if not loop.last %} union all {% endif %}
- {% endfor %}
-{% if not loop.last %} union all {% endif %}
-{% endfor %}
+usps_migration as (select * from {{ ref('stg_usps_migration_union') }}),
+zctas as (select * from {{ ref('zctas') }}),
+zip_codes_to_zctas as (select * from {{ ref('zip_codes_to_zctas') }})
+select
+ usps_migration.*,
+ zctas.zcta_id
+from
+ usps_migration
+ left join zip_codes_to_zctas using zip_code
+ left join zctas
+ on zip_codes_to_zctas.zcta = zctas.zcta and
+ and usps_migration.date_ <@ zctas.valid
diff --git a/dbt/models/zctas.sql b/dbt/models/zctas.sql
new file mode 100644
index 00000000..62212a9b
--- /dev/null
+++ b/dbt/models/zctas.sql
@@ -0,0 +1,28 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['zcta_id'], 'unique': true},
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
+with
+zctas as (
+select
+ zcta,
+ '[2020-01-01,)'::daterange as valid,
+ geom
+from {{ ref('stg_zctas_2020') }}
+union all
+select
+ zcta,
+ '[,2020-01-01)'::daterange as valid,
+ geom
+from {{ ref('stg_zctas_2010') }}
+)
+select
+ {{ dbt_utils.generate_surrogate_key(['zcta', 'valid']) }} as zcta_id,
+ zctas.*
+from zctas
diff --git a/dbt/models/zip_codes.sql b/dbt/models/zip_codes.sql
deleted file mode 100644
index 078f14ef..00000000
--- a/dbt/models/zip_codes.sql
+++ /dev/null
@@ -1,28 +0,0 @@
-{{
- config(
- materialized='table',
- indexes = [
- {'columns': ['zip_code_id'], 'unique': true},
- {'columns': ['valid', 'geom'], 'type': 'gist'}
- ]
- )
-}}
-
-with
-zip_codes as (
-select
- zip_code,
- '[2020-01-01,)'::daterange as valid,
- geom
-from {{ ref('stg_zip_codes_2020') }}
-union all
-select
- zip_code,
- '[,2020-01-01)'::daterange as valid,
- geom
-from {{ ref('stg_zip_codes_2010') }}
-)
-select
- {{ dbt_utils.generate_surrogate_key(['zip_code', 'valid']) }} as zip_code_id,
- zip_codes.*
-from zip_codes
diff --git a/dbt/models/zip_codes_to_zctas.sql b/dbt/models/zip_codes_to_zctas.sql
new file mode 100644
index 00000000..84bffa64
--- /dev/null
+++ b/dbt/models/zip_codes_to_zctas.sql
@@ -0,0 +1,2 @@
+select zip_code, zcta
+from {{ source('minneapolis', 'zip_codes_zcta_xref') }}
From 6639fa0dc117ee970c24e9387c03d642f9b4707e Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 14:28:57 -0400
Subject: [PATCH 122/142] add new api code
---
api/main.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 78 insertions(+)
create mode 100644 api/main.py
diff --git a/api/main.py b/api/main.py
new file mode 100644
index 00000000..ca217e29
--- /dev/null
+++ b/api/main.py
@@ -0,0 +1,78 @@
+import os
+
+from fastapi import FastAPI, Depends
+import psycopg2
+
+# from cities.deployment.tracts_minneapolis.predict import TractsModelPredictor
+
+USERNAME = os.getenv("USERNAME")
+PASSWORD = os.getenv("PASSWORD")
+HOST = os.getenv("HOST")
+DATABASE = os.getenv("DATABASE")
+
+app = FastAPI()
+
+
+def get_db() -> psycopg2.extensions.connection:
+ db = psycopg2.connect(
+ host=HOST, database=DATABASE, user=USERNAME, password=PASSWORD
+ )
+ try:
+ yield db
+ finally:
+ db.close()
+
+
+# def get_predictor(
+# db: psycopg2.extensions.connection = Depends(get_db),
+# ) -> TractsModelPredictor:
+# return TractsModelPredictor(db)
+
+
+@app.get("/demographics")
+async def read_demographics(category: str, db=Depends(get_db)):
+ cur = db.cursor()
+ cur.execute("select * from api__demographics where description = %s", (category,))
+ return cur.fetchall()
+
+
+@app.get("/census_tracts")
+async def read_census_tracts(year: int, db=Depends(get_db)):
+ cur = db.cursor()
+ cur.execute(
+ """
+ with census_tracts as (
+ select census_tract, geom from api__census_tracts
+ where year_ = %s
+ )
+ select json_build_object('type', 'FeatureCollection', 'features', json_agg(ST_AsGeoJSON(census_tracts.*)::json))
+ from census_tracts
+ """,
+ (year,),
+ )
+ return cur.fetchall()
+
+
+@app.get("/high_frequency_transit_lines")
+async def read_census_tracts(year: int, db=Depends(get_db)):
+ cur = db.cursor()
+ cur.execute(
+ """
+ with census_tracts as (
+ select census_tract, geom from api__census_tracts
+ where year_ = %s
+ )
+ select json_build_object('type', 'FeatureCollection', 'features', json_agg(ST_AsGeoJSON(census_tracts.*)::json))
+ from census_tracts
+ """,
+ (year,),
+ )
+ return cur.fetchall()
+
+
+# @app.get("/predict")
+# async def read_predict(
+# samples=100, predictor: TractsModelPredictor = Depends(get_predictor)
+# ):
+# result = predictor.predict(samples=samples)
+# return result.tolist()
From 7b818b91fcd268c5b24e43a2aee7c0704ae0cf59 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 15:04:01 -0400
Subject: [PATCH 123/142] continue refactoring zip codes to zctas
---
dbt/models/docs.md | 4 +++
dbt/models/fair_market_rents.sql | 25 +++++----------
dbt/models/parcels.sql | 2 +-
dbt/models/parking.sql | 12 ++++---
dbt/models/schema.yml | 1 -
.../stg_fair_market_rents_add_zcta.sql | 18 +++++++++++
dbt/models/staging/stg_parcels.sql | 23 +++++++++++--
dbt/models/staging/stg_parking.sql | 2 +-
.../staging/stg_usps_migration_add_zcta.sql | 13 ++++++++
.../staging/stg_usps_migration_union.sql | 32 +++++++++----------
.../staging/stg_usps_migration_unpivot.sql | 9 ++----
.../tracts_model_int__parcels_filtered.sql | 15 +++++++--
dbt/models/usps_migration.sql | 24 ++++++--------
dbt/models/zip_codes_to_zctas.sql | 10 ++++++
14 files changed, 124 insertions(+), 66 deletions(-)
create mode 100644 dbt/models/staging/stg_fair_market_rents_add_zcta.sql
create mode 100644 dbt/models/staging/stg_usps_migration_add_zcta.sql
diff --git a/dbt/models/docs.md b/dbt/models/docs.md
index 02494f2e..2342912d 100644
--- a/dbt/models/docs.md
+++ b/dbt/models/docs.md
@@ -122,4 +122,8 @@ directions are either `from` (out of) the zip code or `to` (in to) the zip code.
Flow types are one of `business`, `family`, `individual`, `perm` (permanent),
`temp` (temporary), or `total`.
+We associate zip codes to ZCTAs and provide aggregate flows for ZCTAs. Note that
+some zip codes do not find a match in our zip to ZCTA mapping table, so there is
+some missingness in this data.
+
{% enddocs %}
diff --git a/dbt/models/fair_market_rents.sql b/dbt/models/fair_market_rents.sql
index c82afc4b..620c0457 100644
--- a/dbt/models/fair_market_rents.sql
+++ b/dbt/models/fair_market_rents.sql
@@ -2,26 +2,17 @@
config(
materialized='table',
indexes = [
- {'columns': ['zip_code_id', 'year_', 'num_bedrooms']}
+ {'columns': ['zcta_id', 'year_', 'num_bedrooms']}
]
)
}}
with
-stg_fair_market_rents_unpivot as (
- select * from {{ ref('stg_fair_market_rents_unpivot') }}
-),
-zip_codes_to_zctas as (select * from {{ ref('zip_codes_to_zctas') }}),
-zctas as (select * from {{ ref('zctas') }})
+fair_market_rents as (select * from {{ ref('stg_fair_market_rents_add_zcta') }})
select
- stg_fair_market_rents_unpivot.zip_code,
- stg_fair_market_rents_unpivot.year_::smallint,
- stg_fair_market_rents_unpivot.num_bedrooms::smallint,
- stg_fair_market_rents_unpivot.rent::smallint,
- zctas.zcta_id
-from
- stg_fair_market_rents_unpivot
- left join zip_codes_to_zctas using zip_code
- left join zctas
- on zip_codes_to_zctas.zcta = zctas.zcta
- and (stg_fair_market_rents_unpivot.year_ || '-01-01')::date <@ zctas.valid
+ zcta_id,
+ year_::smallint,
+ num_bedrooms::smallint,
+ avg(rent) as rent
+from fair_market_rents
+group by 1,2,3
diff --git a/dbt/models/parcels.sql b/dbt/models/parcels.sql
index 9897974f..3cc0f915 100644
--- a/dbt/models/parcels.sql
+++ b/dbt/models/parcels.sql
@@ -20,6 +20,6 @@ select
, census_bgs.census_tract_id
from
parcels
- left join to_zip_codes using (parcel_id)
+ left join to_zctas using (parcel_id)
left join to_census_bgs using (parcel_id)
left join census_bgs using (census_block_group_id)
diff --git a/dbt/models/parking.sql b/dbt/models/parking.sql
index e49574fe..717db5a2 100644
--- a/dbt/models/parking.sql
+++ b/dbt/models/parking.sql
@@ -11,14 +11,18 @@
with
stg_parking as (select * from {{ ref('stg_parking') }}),
stg_parking_to_parcels as (select * from {{ ref('stg_parking_to_parcels') }}),
+ stg_parking_to_first_parcel as (
+ select parking_id, min(parcel_id) as parcel_id
+ from stg_parking_to_parcels group by 1
+ ),
parcels as (select * from {{ ref('parcels') }})
select
stg_parking.*,
- stg_parking_to_parcels.parcel_id,
+ stg_parking_to_first_parcel.parcel_id,
parcels.census_block_group_id,
parcels.census_tract_id,
- parcels.zip_code_id
+ parcels.zcta_id
from
stg_parking
- left join stg_parking_to_parcels using parking_id
- left join parcels using parcel_id
+ left join stg_parking_to_first_parcel using (parking_id)
+ left join parcels using (parcel_id)
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index 52800c91..d807cb4a 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -177,7 +177,6 @@ models:
columns:
- name: zcta_id
data_tests:
- - not_null
- relationships:
to: ref('zctas')
field: zcta_id
diff --git a/dbt/models/staging/stg_fair_market_rents_add_zcta.sql b/dbt/models/staging/stg_fair_market_rents_add_zcta.sql
new file mode 100644
index 00000000..30bee443
--- /dev/null
+++ b/dbt/models/staging/stg_fair_market_rents_add_zcta.sql
@@ -0,0 +1,18 @@
+with
+stg_fair_market_rents_unpivot as (
+ select * from {{ ref('stg_fair_market_rents_unpivot') }}
+),
+zip_codes_to_zctas as (select * from {{ ref('zip_codes_to_zctas') }}),
+zctas as (select * from {{ ref('zctas') }})
+select
+ stg_fair_market_rents_unpivot.zip_code,
+ stg_fair_market_rents_unpivot.year_::smallint,
+ stg_fair_market_rents_unpivot.num_bedrooms::smallint,
+ stg_fair_market_rents_unpivot.rent::smallint,
+ zctas.zcta_id
+from
+ stg_fair_market_rents_unpivot
+ left join zip_codes_to_zctas using (zip_code)
+ left join zctas
+ on zip_codes_to_zctas.zcta = zctas.zcta
+ and (stg_fair_market_rents_unpivot.year_ || '-01-01')::date <@ zctas.valid
diff --git a/dbt/models/staging/stg_parcels.sql b/dbt/models/staging/stg_parcels.sql
index 9ffc665e..83b9c77a 100644
--- a/dbt/models/staging/stg_parcels.sql
+++ b/dbt/models/staging/stg_parcels.sql
@@ -1,3 +1,13 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['parcel_id'], 'unique': true},
+ {'columns': ['valid', 'geom'], 'type': 'gist'}
+ ]
+ )
+}}
+
{% set years = range(2002, 2024) %}
{% set city = 'MINNEAPOLIS' %}
{% set county_id = '053' %}
@@ -15,7 +25,7 @@ parcels_union as (
nullif(emv_land, 0)::int as emv_land,
nullif(emv_bldg, 0)::int as emv_bldg,
nullif(emv_total, 0)::int as emv_total,
- nullif(year_built, 0)::int as year_built,
+ nullif(year_built, 0)::smallint as year_built,
nullif(sale_date, '1899-12-30'::date) as sale_date,
nullif(sale_value, 0)::int as sale_value,
st_transform(geom, {{ var("srid") }}) as geom
@@ -32,5 +42,14 @@ parcels_distinct as (
from parcels_union
)
select
- {{ dbt_utils.generate_surrogate_key(['ogc_fid', 'valid']) }} as parcel_id, *
+ {{ dbt_utils.generate_surrogate_key(['ogc_fid', 'valid']) }} as parcel_id,
+ pin,
+ valid,
+ emv_land,
+ emv_bldg,
+ emv_total,
+ year_built,
+ sale_date,
+ sale_value,
+ geom
from parcels_distinct
diff --git a/dbt/models/staging/stg_parking.sql b/dbt/models/staging/stg_parking.sql
index ed00a8b1..61667cb0 100644
--- a/dbt/models/staging/stg_parking.sql
+++ b/dbt/models/staging/stg_parking.sql
@@ -10,6 +10,6 @@ select
, "downtown y" = 'Y' as is_downtown
, "housing un"::smallint as num_housing_units
, "car parkin"::smallint as num_car_parking_spaces
- , "bike parki"::smallint as num_bike_parking_spaces
+ , replace("bike parki", ',', '')::smallint as num_bike_parking_spaces
, st_transform(geom, {{ var("srid") }}) as geom
from parking_raw
diff --git a/dbt/models/staging/stg_usps_migration_add_zcta.sql b/dbt/models/staging/stg_usps_migration_add_zcta.sql
new file mode 100644
index 00000000..4097c6b2
--- /dev/null
+++ b/dbt/models/staging/stg_usps_migration_add_zcta.sql
@@ -0,0 +1,13 @@
+with
+usps_migration as (select * from {{ ref('stg_usps_migration_unpivot') }}),
+zctas as (select * from {{ ref('zctas') }}),
+zip_codes_to_zctas as (select * from {{ ref('zip_codes_to_zctas') }})
+select
+ usps_migration.*,
+ zctas.zcta_id
+from
+ usps_migration
+ left join zip_codes_to_zctas using (zip_code)
+ left join zctas
+ on zip_codes_to_zctas.zcta = zctas.zcta
+ and usps_migration.date_ <@ zctas.valid
diff --git a/dbt/models/staging/stg_usps_migration_union.sql b/dbt/models/staging/stg_usps_migration_union.sql
index e1e790e7..4ab16fb4 100644
--- a/dbt/models/staging/stg_usps_migration_union.sql
+++ b/dbt/models/staging/stg_usps_migration_union.sql
@@ -2,22 +2,22 @@
{% for year_ in years %}
select
- "YYYYMM" as yyyy_mm
- , "ZIPCODE" as zip_code
- , "CITY" as city
- , "STATE" as state_
- , "TOTAL_FROM_ZIP" as total_from_zip
- , "TOTAL_BUSINESS" as total_from_zip_business
- , "TOTAL_FAMILY" as total_from_zip_family
- , "TOTAL_INDIVIDUAL" as total_from_zip_individual
- , "TOTAL_PERM" as total_from_zip_perm
- , "TOTAL_TEMP" as total_from_zip_temp
- , "TOTAL_TO_ZIP" as total_to_zip
- , "TOTAL_BUSINESS_dup" as total_to_zip_business
- , "TOTAL_FAMILY_dup" as total_to_zip_family
- , "TOTAL_INDIVIDUAL_dup" as total_to_zip_individual
- , "TOTAL_PERM_dup" as total_to_zip_perm
- , "TOTAL_TEMP_dup" as total_to_zip_temp
+ to_date("YYYYMM", 'YYYYMM') as date_,
+ replace("ZIPCODE", '=', '') as zip_code,
+ "CITY" as city,
+ "STATE" as state_,
+ "TOTAL_FROM_ZIP" as total_from_zip,
+ "TOTAL_BUSINESS" as total_from_zip_business,
+ "TOTAL_FAMILY" as total_from_zip_family,
+ "TOTAL_INDIVIDUAL" as total_from_zip_individual,
+ "TOTAL_PERM" as total_from_zip_perm,
+ "TOTAL_TEMP" as total_from_zip_temp,
+ "TOTAL_TO_ZIP" as total_to_zip,
+ "TOTAL_BUSINESS_dup" as total_to_zip_business,
+ "TOTAL_FAMILY_dup" as total_to_zip_family,
+ "TOTAL_INDIVIDUAL_dup" as total_to_zip_individual,
+ "TOTAL_PERM_dup" as total_to_zip_perm,
+ "TOTAL_TEMP_dup" as total_to_zip_temp
from {{ source('minneapolis', 'usps_y' ~ year_) }}
{% if not loop.last %} union all {% endif %}
{% endfor %}
diff --git a/dbt/models/staging/stg_usps_migration_unpivot.sql b/dbt/models/staging/stg_usps_migration_unpivot.sql
index d8ba1c49..86dee67e 100644
--- a/dbt/models/staging/stg_usps_migration_unpivot.sql
+++ b/dbt/models/staging/stg_usps_migration_unpivot.sql
@@ -2,10 +2,7 @@
{% set usps_migration_flow_directions = ['from', 'to'] %}
with
-process_date as (
- select to_date(yyyy_mm, 'YYYYMM') as date_, *
- from {{ ref('stg_usps_migration_union') }}
-)
+usps_migration as (select * from {{ ref('stg_usps_migration_union') }})
{% for flow_direction in usps_migration_flow_directions %}
select
date_
@@ -13,7 +10,7 @@ process_date as (
, '{{ flow_direction }}' as flow_direction
, 'total' as flow_type
, total_{{ flow_direction }}_zip::int as flow_value
- from process_date
+ from usps_migration
union all
{% for flow_type in usps_migration_flow_types %}
select
@@ -22,7 +19,7 @@ process_date as (
, '{{ flow_direction }}' as flow_direction
, '{{ flow_type }}' as flow_type
, total_{{ flow_direction }}_zip_{{ flow_type }}::int as flow_value
- from process_date
+ from usps_migration
{% if not loop.last %} union all {% endif %}
{% endfor %}
{% if not loop.last %} union all {% endif %}
diff --git a/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
index 80055cc2..42b97bef 100644
--- a/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
+++ b/dbt/models/tracts_model/intermediate/tracts_model_int__parcels_filtered.sql
@@ -8,7 +8,6 @@
with
census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered') }}),
parcels as (select * from {{ ref('parcels') }}),
-
parcels_tag as (select parcel_id as id, valid, geom from parcels),
census_tracts_tag as (select census_tract_id as id, valid, geom from census_tracts),
parcels_to_census_tracts as (
@@ -17,6 +16,16 @@ parcels_to_census_tracts as (
parent_id as census_tract_id
from {{ tag_regions("parcels_tag", "census_tracts_tag") }}
)
-
-select parcels.*, parcels_to_census_tracts.census_tract_id
+select
+ parcels.parcel_id,
+ parcels.pin,
+ parcels.valid,
+ parcels.emv_land,
+ parcels.emv_bldg,
+ parcels.emv_total,
+ parcels.year_built,
+ parcels.sale_date,
+ parcels.sale_value,
+ parcels.geom,
+ parcels_to_census_tracts.census_tract_id
from parcels join parcels_to_census_tracts using (parcel_id)
diff --git a/dbt/models/usps_migration.sql b/dbt/models/usps_migration.sql
index ed028cdc..d7b1fc73 100644
--- a/dbt/models/usps_migration.sql
+++ b/dbt/models/usps_migration.sql
@@ -2,24 +2,18 @@
config(
materialized='table',
indexes = [
- {'columns': ['date_', 'zip_code_id', 'flow_direction', 'flow_type'], 'unique': true},
+ {'columns': ['date_', 'zcta_id', 'flow_direction', 'flow_type'], 'unique': true},
]
)
}}
-{% set usps_migration_flow_types = ['business', 'family', 'individual', 'perm', 'temp'] %}
-{% set usps_migration_flow_directions = ['from', 'to'] %}
-
with
-usps_migration as (select * from {{ ref('stg_usps_migration_union') }}),
-zctas as (select * from {{ ref('zctas') }}),
-zip_codes_to_zctas as (select * from {{ ref('zip_codes_to_zctas') }})
+usps_migration as (select * from {{ ref('stg_usps_migration_add_zcta') }})
select
- usps_migration.*,
- zctas.zcta_id
-from
- usps_migration
- left join zip_codes_to_zctas using zip_code
- left join zctas
- on zip_codes_to_zctas.zcta = zctas.zcta and
- and usps_migration.date_ <@ zctas.valid
+ date_,
+ flow_direction,
+ flow_type,
+ zcta_id,
+ sum(flow_value) as flow_value
+from usps_migration
+group by 1,2,3,4
diff --git a/dbt/models/zip_codes_to_zctas.sql b/dbt/models/zip_codes_to_zctas.sql
index 84bffa64..9ac3a70f 100644
--- a/dbt/models/zip_codes_to_zctas.sql
+++ b/dbt/models/zip_codes_to_zctas.sql
@@ -1,2 +1,12 @@
+{{
+ config(
+ materialized='table',
+ indexes = [
+ {'columns': ['zip_code']},
+ {'columns': ['zcta']}
+ ]
+ )
+}}
+
select zip_code, zcta
from {{ source('minneapolis', 'zip_codes_zcta_xref') }}
From f3c2fc18920610101a9857d93c46a61461344039 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 15:04:24 -0400
Subject: [PATCH 124/142] refactor load_server to no longer require the
password as an env var
---
load_data_server/load_server.py | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/load_data_server/load_server.py b/load_data_server/load_server.py
index 8c3089e8..a68fad0d 100644
--- a/load_data_server/load_server.py
+++ b/load_data_server/load_server.py
@@ -32,7 +32,6 @@
HOST = os.getenv("HOST")
DATABASE = os.getenv("DATABASE")
USERNAME = os.getenv("USERNAME")
-PASSWORD = os.getenv("PASSWORD")
OGR2OGR_OPTS = [
"--config",
@@ -47,9 +46,7 @@
"-nlt",
"PROMOTE_TO_MULTI",
]
-DB_OPTS = [
- f"PG:dbname={DATABASE} host={HOST} user={USERNAME} password={PASSWORD} port=5432"
-]
+DB_OPTS = [f"PG:dbname={DATABASE} host={HOST} user={USERNAME} port=5432"]
MAX_RETRIES = 3
RETRY_DELAY = 5 # seconds
@@ -59,9 +56,7 @@ def get_db_connection():
"""Create a database connection with retries."""
for attempt in range(MAX_RETRIES):
try:
- conn = psycopg2.connect(
- host=HOST, database=DATABASE, user=USERNAME, password=PASSWORD
- )
+ conn = psycopg2.connect(f"postgresql://{USERNAME}@{HOST}/{DATABASE}")
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
return conn
except psycopg2.OperationalError as e:
From 76fa4a7bb21f35a85716ba2e01f3436600c375aa Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 15:31:53 -0400
Subject: [PATCH 125/142] handle one to many mapping problem
---
dbt/models/commercial_permits.sql | 13 +++++++++----
dbt/models/residential_permits.sql | 12 ++++++++----
2 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/dbt/models/commercial_permits.sql b/dbt/models/commercial_permits.sql
index 58961e23..755de463 100644
--- a/dbt/models/commercial_permits.sql
+++ b/dbt/models/commercial_permits.sql
@@ -11,14 +11,19 @@
with
stg_commercial_permits as (select * from {{ ref('stg_commercial_permits') }}),
stg_commercial_permits_to_parcels as (select * from {{ ref('stg_commercial_permits_to_parcels') }}),
+permits_to_first_parcel as (
+ select commercial_permit_id, min(parcel_id) as parcel_id
+ from stg_commercial_permits_to_parcels group by 1
+),
+
parcels as (select * from {{ ref('parcels') }})
select
stg_commercial_permits.*,
- stg_commercial_permits_to_parcels.parcel_id,
+ permits_to_first_parcel.parcel_id,
parcels.census_block_group_id,
parcels.census_tract_id,
- parcels.zip_code_id
+ parcels.zcta_id
from
stg_commercial_permits
- left join stg_commercial_permits_to_parcels using commercial_permit_id
- left join parcels using parcel_id
+ left join permits_to_first_parcel using (commercial_permit_id)
+ left join parcels using (parcel_id)
diff --git a/dbt/models/residential_permits.sql b/dbt/models/residential_permits.sql
index bcba6ab4..6613e374 100644
--- a/dbt/models/residential_permits.sql
+++ b/dbt/models/residential_permits.sql
@@ -11,14 +11,18 @@
with
stg_residential_permits as (select * from {{ ref('stg_residential_permits') }}),
stg_residential_permits_to_parcels as (select * from {{ ref('stg_residential_permits_to_parcels') }}),
+permits_to_first_parcel as (
+ select residential_permit_id, min(parcel_id) as parcel_id
+ from stg_residential_permits_to_parcels group by 1
+),
parcels as (select * from {{ ref('parcels') }})
select
stg_residential_permits.*,
- stg_residential_permits_to_parcels.parcel_id,
+ permits_to_first_parcel.parcel_id,
parcels.census_block_group_id,
parcels.census_tract_id,
- parcels.zip_code_id
+ parcels.zcta_id
from
stg_residential_permits
- left join stg_residential_permits_to_parcels using residential_permit_id
- left join parcels using parcel_id
+ left join permits_to_first_parcel using (residential_permit_id)
+ left join parcels using (parcel_id)
From 85d8325da2a3de4e0043a78a81e74503fff94e45 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 15:32:06 -0400
Subject: [PATCH 126/142] add more documentation
---
dbt/models/docs.md | 55 +++++++++++++++++++++++++++++++++++++++++++
dbt/models/schema.yml | 17 +++++++++++++
2 files changed, 72 insertions(+)
diff --git a/dbt/models/docs.md b/dbt/models/docs.md
index 2342912d..fd74fa38 100644
--- a/dbt/models/docs.md
+++ b/dbt/models/docs.md
@@ -5,6 +5,10 @@ Contains commercial building permit applications.
Notes:
- Permits are filtered to only include those in Minneapolis.
- `square_feet` is treated as missing if it is 0.
+ - When mapping permits to parcels, if more than one parcel contains the permit
+ location, a parcel will be chosen arbitrarily. This can happen because the
+ same parcel spatial extent can appear multiple times with different PINs, to
+ represent e.g. units in a condominium.
{% enddocs %}
@@ -16,6 +20,16 @@ Notes:
- Permits are filtered to only include those in Minneapolis.
- `square_feet` is treated as missing if it is 0.
- `permit_value` is treated as missing if it is 0.
+ - If more than one parcel contains the permit location, a parcel is selected
+ arbitrarily. See `commercial_permits`.
+
+{% enddocs %}
+
+{% docs parking %}
+
+Notes:
+ - If more than one parcel contains the permit location, a parcel is selected
+ arbitrarily. See `commercial_permits`.
{% enddocs %}
@@ -127,3 +141,44 @@ some zip codes do not find a match in our zip to ZCTA mapping table, so there is
some missingness in this data.
{% enddocs %}
+
+{% docs demographics %}
+
+Contains demographic data at census tract granularity.
+Combines ACS data and segregation indexes in one table.
+
+Notes:
+- Fills in missing demographic data from 2011 and 2012 with data from 2013.
+- Replaces pandemic-affected data from 2020 with data from 2019.
+
+{% enddocs %}
+
+{% docs neighborhoods %}
+
+Neighborhood boundaries in the city of Minneapolis.
+
+{% enddocs %}
+
+{% docs wards %}
+
+Ward boundaries in the city of Minneapolis.
+
+{% enddocs %}
+
+{% docs university %}
+
+Boundary of the University of Minnesota.
+
+{% enddocs %}
+
+{% docs downtown %}
+
+Boundary of the downtown of Minneapolis.
+
+{% enddocs %}
+
+{% docs city_boundary %}
+
+Boundary of the city of Minneapolis.
+
+{% enddocs %}
diff --git a/dbt/models/schema.yml b/dbt/models/schema.yml
index d807cb4a..e3948f2d 100644
--- a/dbt/models/schema.yml
+++ b/dbt/models/schema.yml
@@ -123,6 +123,21 @@ models:
- name: high_frequency_transit_lines
description: '{{ doc("high_frequency_transit_lines") }}'
+ - name: demographics
+ description: '{{ doc("demographics") }}'
+
+ - name: university
+ description: '{{ doc("university") }}'
+
+ - name: downtown
+ description: '{{ doc("downtown") }}'
+
+ - name: city_boundary
+ description: '{{ doc("city_boundary") }}'
+
+ - name: parking
+ description: '{{ doc("parking") }}'
+
- name: segregation_indexes
description: '{{ doc("segregation_indexes") }}'
data_tests:
@@ -198,6 +213,7 @@ models:
- unique
- name: neighborhoods
+ description: '{{ doc("neighborhoods") }}'
columns:
- name: neighborhood_id
data_tests:
@@ -205,6 +221,7 @@ models:
- unique
- name: wards
+ description: '{{ doc("wards") }}'
columns:
- name: ward_id
data_tests:
From e52b63785c0c6bb1a587ef20472949e32839a8fd Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 16:12:23 -0400
Subject: [PATCH 127/142] materialize for performance
---
dbt/models/staging/stg_usps_migration_add_zcta.sql | 6 ++++++
dbt/models/staging/stg_usps_migration_unpivot.sql | 6 ++++++
2 files changed, 12 insertions(+)
diff --git a/dbt/models/staging/stg_usps_migration_add_zcta.sql b/dbt/models/staging/stg_usps_migration_add_zcta.sql
index 4097c6b2..2b45f38e 100644
--- a/dbt/models/staging/stg_usps_migration_add_zcta.sql
+++ b/dbt/models/staging/stg_usps_migration_add_zcta.sql
@@ -1,3 +1,9 @@
+{{
+ config(
+ materialized='table'
+ )
+}}
+
with
usps_migration as (select * from {{ ref('stg_usps_migration_unpivot') }}),
zctas as (select * from {{ ref('zctas') }}),
diff --git a/dbt/models/staging/stg_usps_migration_unpivot.sql b/dbt/models/staging/stg_usps_migration_unpivot.sql
index 86dee67e..5f358c4b 100644
--- a/dbt/models/staging/stg_usps_migration_unpivot.sql
+++ b/dbt/models/staging/stg_usps_migration_unpivot.sql
@@ -1,3 +1,9 @@
+{{
+ config(
+ materialized='table'
+ )
+}}
+
{% set usps_migration_flow_types = ['business', 'family', 'individual', 'perm', 'temp'] %}
{% set usps_migration_flow_directions = ['from', 'to'] %}
From 744d8b6160e8a8bcbb3667dab8df12d925343dd5 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Wed, 4 Sep 2024 16:14:37 -0400
Subject: [PATCH 128/142] add .env
---
.env | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 .env
diff --git a/.env b/.env
new file mode 100644
index 00000000..f143c46d
--- /dev/null
+++ b/.env
@@ -0,0 +1,6 @@
+GOOGLE_CLOUD_PROJECT=cities-429602
+GOOGLE_CLOUD_BUCKET=minneapolis-basis
+SCHEMA=minneapolis
+HOST=34.123.100.76
+DATABASE=cities
+USERNAME=postgres
From 266d24fea609e0058453d35c84c82e3f6fa2ee34 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 5 Sep 2024 13:39:00 -0400
Subject: [PATCH 129/142] add mean and median parcel areas per tract
---
.../tracts_model/intermediate/census_tracts_parcel_area.sql | 4 +++-
dbt/models/tracts_model/tracts_model__census_tracts.sql | 4 +++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql b/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
index cb6760fe..1f4216e7 100644
--- a/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
+++ b/dbt/models/tracts_model/intermediate/census_tracts_parcel_area.sql
@@ -3,7 +3,9 @@ census_tracts as (select * from {{ ref('tracts_model_int__census_tracts_filtered
parcels as (select * from {{ ref('tracts_model_int__parcels_filtered') }})
select
census_tract_id,
- sum(st_area(parcels.geom)) as parcel_sqm
+ sum(st_area(parcels.geom)) as parcel_sqm,
+ avg(st_area(parcels.geom)) as parcel_mean_sqm,
+ {{ median('st_area(parcels.geom)') }} as parcel_median_sqm
from
census_tracts left join parcels using (census_tract_id)
group by 1
diff --git a/dbt/models/tracts_model/tracts_model__census_tracts.sql b/dbt/models/tracts_model/tracts_model__census_tracts.sql
index 8d584472..aa6ba710 100644
--- a/dbt/models/tracts_model/tracts_model__census_tracts.sql
+++ b/dbt/models/tracts_model/tracts_model__census_tracts.sql
@@ -44,7 +44,9 @@ select
, property_values.median_value
, distance_to_transit.median_distance_to_transit as median_distance
, distance_to_transit.mean_distance_to_transit as mean_distance
- , parcel_area.parcel_sqm
+ , parcel_area.parcel_sqm::double precision
+ , parcel_area.parcel_mean_sqm::double precision
+ , parcel_area.parcel_median_sqm::double precision
, parking_limits.mean_limit::double precision
, white_frac.value_ as white
, income.value_ as income
From f4a5af4d82917c4734199147737e24f4430c46a6 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 5 Sep 2024 13:41:31 -0400
Subject: [PATCH 130/142] forgot to standardize new columns
---
dbt/models/tracts_model/tracts_model__census_tracts.sql | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dbt/models/tracts_model/tracts_model__census_tracts.sql b/dbt/models/tracts_model/tracts_model__census_tracts.sql
index aa6ba710..0e7e1ea4 100644
--- a/dbt/models/tracts_model/tracts_model__census_tracts.sql
+++ b/dbt/models/tracts_model/tracts_model__census_tracts.sql
@@ -68,7 +68,8 @@ select
, {{ standardize_cat(['year']) }}
, {{ standardize_cont(['housing_units', 'total_value', 'median_value',
'median_distance', 'mean_distance', 'parcel_sqm',
- 'white', 'income', 'mean_limit', 'segregation' ]) }}
+ 'parcel_mean_sqm', 'parcel_median_sqm', 'white',
+ 'income', 'mean_limit', 'segregation' ]) }}
from
raw_data
)
From 1babc51484e3b65caddd9e4a264fcfdef4d6946a Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Thu, 5 Sep 2024 15:52:00 -0400
Subject: [PATCH 131/142] add temp high frequency transit lines endpoint
---
api/schema.sql | 49 ++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 44 insertions(+), 5 deletions(-)
diff --git a/api/schema.sql b/api/schema.sql
index 42fd4e4c..2285c2b7 100644
--- a/api/schema.sql
+++ b/api/schema.sql
@@ -1,3 +1,4 @@
+begin;
drop schema if exists api cascade;
create schema api;
@@ -10,9 +11,37 @@ create view api.census_tracts as (
select * from api__census_tracts
);
-create view api.high_frequency_transit_lines as (
- select * from api__high_frequency_transit_lines
-);
+create function api.high_frequency_transit_lines() returns setof dev.api__high_frequency_transit_lines as $$
+ select * from dev.api__high_frequency_transit_lines
+$$ language sql;
+
+create function api.high_frequency_transit_lines(
+ blue_zone_radius double precision,
+ yellow_zone_line_radius double precision,
+ yellow_zone_stop_radius double precision
+) returns table (
+ valid daterange,
+ geom geometry(LineString, 4269),
+ blue_zone_geom geometry(LineString, 4269),
+ yellow_zone_geom geometry(Geometry, 4269)
+) as $$
+ with
+ lines as (select * from dev.stg_high_frequency_transit_lines_union),
+ stops as (select * from dev.high_frequency_transit_stops),
+ lines_and_stops as (
+ select
+ lines.valid * stops.valid as valid,
+ lines.geom as line_geom,
+ stops.geom as stop_geom
+ from lines inner join stops on lines.valid && stops.valid
+ )
+ select
+ valid,
+ st_transform(line_geom, 4269) as geom,
+ st_transform(st_buffer(line_geom, blue_zone_radius), 4269) as blue_zone_geom,
+ st_transform(st_union(st_buffer(line_geom, yellow_zone_line_radius), st_buffer(stop_geom, yellow_zone_stop_radius)), 4269) as yellow_zone_geom
+ from lines_and_stops
+$$ language sql;
do $$
begin
@@ -21,8 +50,18 @@ exception when duplicate_object then raise notice '%, skipping', sqlerrm using e
end
$$;
-grant usage on schema public to web_anon;
+grant all on schema public to web_anon;
+grant all on schema dev to web_anon;
grant select on table public.spatial_ref_sys TO web_anon;
grant usage on schema api to web_anon;
-grant select on all tables in schema api to web_anon;
+grant all on all tables in schema api to web_anon;
+grant all on all functions in schema api to web_anon;
+grant all on schema api to web_anon;
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA dev TO web_anon;
+GRANT ALL PRIVILEGES ON ALL functions IN SCHEMA dev TO web_anon;
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA api TO web_anon;
+GRANT ALL PRIVILEGES ON ALL functions IN SCHEMA api TO web_anon;
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO web_anon;
+GRANT ALL PRIVILEGES ON ALL functions IN SCHEMA public TO web_anon;
grant web_anon to postgres;
+commit;
From 801b7a76a4ebb3d505756d1288876fe73728b795 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 6 Sep 2024 12:41:29 -0400
Subject: [PATCH 132/142] add more documentation to tracts model tables
---
dbt/models/tracts_model/docs.md | 92 ++++++++++++++++++++++++++++++
dbt/models/tracts_model/schema.yml | 51 +++++++++++++++++
2 files changed, 143 insertions(+)
create mode 100644 dbt/models/tracts_model/docs.md
create mode 100644 dbt/models/tracts_model/schema.yml
diff --git a/dbt/models/tracts_model/docs.md b/dbt/models/tracts_model/docs.md
new file mode 100644
index 00000000..a4a3371e
--- /dev/null
+++ b/dbt/models/tracts_model/docs.md
@@ -0,0 +1,92 @@
+{% docs tracts_model_int__census_tracts_filtered %}
+
+Intermediate table that selects census tracts of interest. Considers only tracts
+in the city boundary (tracts must intersect boundary and have at least 90% of
+area overlapping) and only for years 2011 to 2020.
+
+Notes:
+- Census tracts for 2020 are replaced with tracts for 2019. This requires
+ retagging parcels and other spatial entities, because the `census_tract_id`
+ changes with the replacement.
+
+{% enddocs %}
+
+{% docs tracts_model_int__parcels_filtered %}
+
+Retag parcels to account for tract replacement. This also has the effect of
+filtering parcels to the considered tracts.
+
+{% enddocs %}
+
+{% docs census_tracts_distance_to_transit %}
+
+Aggregate `parcels_distance_to_transit` by tract.
+
+{% enddocs %}
+
+{% docs census_tracts_housing_units %}
+
+Aggregate number of units built by tract. Unit data is drawn from
+`residential_permits`.
+
+{% enddocs %}
+
+{% docs census_tracts_parcel_area %}
+
+Aggregate parcel area by tract. Area is computed from the parcel geometry, not
+from the area included in the parcel dataset.
+
+{% enddocs %}
+
+{% docs census_tracts_parking_limits %}
+
+Parking limits aggregated by tract.
+
+{% enddocs %}
+
+{% docs parcels_distance_to_transit %}
+
+Distance from a parcel to the nearest transit (line or stop). This is the
+smallest distance from the parcel geometry to the line geometry, not from the
+parcel centroid.
+
+{% enddocs %}
+
+{% docs parcels_parking_limits %}
+
+Parking limits by parcel. The parking limit is a function of the distance from
+the parcel to the nearest transit line/transit stop.
+
+Notes:
+- Parcels in all years that intersect (any level of intersection) the downtown
+ area have the limit eliminated.
+- Parcels before 2015 have the full limit.
+- Parcels after 2015 and in the blue zone have the limit eliminated.
+- Parcels after 2015 and in the yellow zone have the limit reduced.
+
+{% enddocs %}
+
+{% docs census_tracts_property_values %}
+
+Total and median property value aggregated by tract. Uses total estimated market
+value from the parcel dataset.
+
+{% enddocs %}
+
+{% docs tracts_model__census_tracts %}
+
+Wide table that joins various census tract level aggregates.
+
+Notes:
+- Continuous columns are standardized by default. Categorical columns are
+ remapped to [0, |D|), where D is the domain. The original value of a column
+ `c` is called `c_original`.
+- Demographic variables are drawn from ACS tract level data.
+
+{% enddocs %}
+
+{% docs tracts_model__parcels %}
+
+Parcels filtered by the considered census tracts, with additional data.
+
+{% enddocs %}
diff --git a/dbt/models/tracts_model/schema.yml b/dbt/models/tracts_model/schema.yml
new file mode 100644
index 00000000..250d415e
--- /dev/null
+++ b/dbt/models/tracts_model/schema.yml
@@ -0,0 +1,51 @@
+models:
+ - name: tracts_model_int__census_tracts_filtered
+ description: '{{ doc("tracts_model_int__census_tracts_filtered") }}'
+
+ - name: tracts_model_int__parcels_filtered
+ description: '{{ doc("tracts_model_int__parcels_filtered") }}'
+
+ - name: tracts_model__census_tracts
+ description: '{{ doc("tracts_model__census_tracts") }}'
+ columns:
+ - name: segregation
+ description: Segregation with respect to the annual city distribution.
+ - name: white
+ description: The proportion of white people in the tract, not the absolute number.
+ - name: income
+ description: Median household income in the tract.
+ - name: median_distance
+ description: Median parcel distance to transit in meters.
+ - name: mean_distance
+ description: Mean parcel distance to transit in meters.
+
+ - name: tracts_model__parcels
+ description: '{{ doc("tracts_model__parcels") }}'
+ columns:
+ - name: distance_to_transit
+ description: Minimum distance to transit (lines or stops) in meters.
+ - name: limit_con
+ description: Numeric representation of parking limit (1 for full, 0 for eliminated, 0.5 for reduced).
+ - name: downtown_yn
+ description: Whether the parcel intersects the downtown area.
+
+ - name: census_tracts_distance_to_transit
+ description: '{{ doc("census_tracts_distance_to_transit") }}'
+
+ - name: census_tracts_housing_units
+ description: '{{ doc("census_tracts_housing_units") }}'
+
+ - name: census_tracts_parcel_area
+ description: '{{ doc("census_tracts_parcel_area") }}'
+
+ - name: census_tracts_parking_limits
+ description: '{{ doc("census_tracts_parking_limits") }}'
+
+ - name: parcels_distance_to_transit
+ description: '{{ doc("parcels_distance_to_transit") }}'
+
+ - name: parcels_parking_limits
+ description: '{{ doc("parcels_parking_limits") }}'
+
+ - name: census_tracts_property_values
+ description: '{{ doc("census_tracts_property_values") }}'
From 85eac69d160644e8fbed57282940058ce920daab Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 6 Sep 2024 12:51:19 -0400
Subject: [PATCH 133/142] remove spurious points from transit lines data
---
.../stg_high_frequency_transit_lines_union.sql | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/dbt/models/staging/stg_high_frequency_transit_lines_union.sql b/dbt/models/staging/stg_high_frequency_transit_lines_union.sql
index 8b361587..56d4b680 100644
--- a/dbt/models/staging/stg_high_frequency_transit_lines_union.sql
+++ b/dbt/models/staging/stg_high_frequency_transit_lines_union.sql
@@ -1,21 +1,24 @@
-with lines_2015 as (
+with
+lines_2015 as (
select
st_union(st_transform(geom, {{ var("srid") }})) as geom
from
{{ source('minneapolis', 'high_frequency_transit_2015_freq_lines') }}
-)
-, lines_2016 as (
+ where st_geometrytype(geom) = 'ST_LineString'
+),
+lines_2016 as (
select
st_union(st_transform(geom, {{ var("srid") }})) as geom
from
{{ source('minneapolis', 'high_frequency_transit_2016_freq_lines') }}
+ where st_geometrytype(geom) = 'ST_LineString'
)
select
- '(,2016-01-01)'::daterange as valid,
- geom
+ '(,2016-01-01)'::daterange as valid,
+ geom
from lines_2015
union all
select
- '[2016-01-01,)'::daterange as valid,
- geom
+ '[2016-01-01,)'::daterange as valid,
+ geom
from lines_2016
From f512ec036ac0201a425432b80c9e62a4b63cf829 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 6 Sep 2024 14:22:50 -0400
Subject: [PATCH 134/142] fix dropped model
---
dbt/models/staging/stg_fair_market_rents_add_zcta.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dbt/models/staging/stg_fair_market_rents_add_zcta.sql b/dbt/models/staging/stg_fair_market_rents_add_zcta.sql
index 30bee443..de2fdcba 100644
--- a/dbt/models/staging/stg_fair_market_rents_add_zcta.sql
+++ b/dbt/models/staging/stg_fair_market_rents_add_zcta.sql
@@ -1,6 +1,6 @@
with
stg_fair_market_rents_unpivot as (
- select * from {{ ref('stg_fair_market_rents_unpivot') }}
+ select * from {{ ref('stg_fair_market_rents_dedup') }}
),
zip_codes_to_zctas as (select * from {{ ref('zip_codes_to_zctas') }}),
zctas as (select * from {{ ref('zctas') }})
From 3d5ccd11ba9aa6aa25ac6f493afc23b2edeb46d0 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 6 Sep 2024 14:26:03 -0400
Subject: [PATCH 135/142] remove unused config
---
.pg_format | 2 --
1 file changed, 2 deletions(-)
delete mode 100644 .pg_format
diff --git a/.pg_format b/.pg_format
deleted file mode 100644
index 2a3c25bb..00000000
--- a/.pg_format
+++ /dev/null
@@ -1,2 +0,0 @@
-keyword-case=1
-comma=start
\ No newline at end of file
From ec72e2ca824beb97e4bfc9aa0de81478acaa2edb Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 6 Sep 2024 14:26:23 -0400
Subject: [PATCH 136/142] drop api from pr
---
api/main.py | 78 ---------------------------------
api/postgrest.conf | 107 ---------------------------------------------
api/schema.sql | 67 ----------------------------
3 files changed, 252 deletions(-)
delete mode 100644 api/main.py
delete mode 100644 api/postgrest.conf
delete mode 100644 api/schema.sql
diff --git a/api/main.py b/api/main.py
deleted file mode 100644
index ca217e29..00000000
--- a/api/main.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import os
-
-from fastapi import FastAPI, Depends
-import psycopg2
-
-# from cities.deployment.tracts_minneapolis.predict import TractsModelPredictor
-
-USERNAME = os.getenv("USERNAME")
-PASSWORD = os.getenv("PASSWORD")
-HOST = os.getenv("HOST")
-DATABASE = os.getenv("DATABASE")
-
-app = FastAPI()
-
-
-def get_db() -> psycopg2.extensions.connection:
- db = psycopg2.connect(
- host=HOST, database=DATABASE, user=USERNAME, password=PASSWORD
- )
- try:
- yield db
- finally:
- db.close()
-
-
-# def get_predictor(
-# db: psycopg2.extensions.connection = Depends(get_db),
-# ) -> TractsModelPredictor:
-# return TractsModelPredictor(db)
-
-
-@app.get("/demographics")
-async def read_demographics(category: str, db=Depends(get_db)):
- cur = db.cursor()
- cur.execute("select * from api__demographics where description = %s", (category,))
- return cur.fetchall()
-
-
-@app.get("/census_tracts")
-async def read_census_tracts(year: int, db=Depends(get_db)):
- cur = db.cursor()
- cur.execute(
- """
- with census_tracts as (
- select census_tract, geom from api__census_tracts
- where year_ = %s
- )
- select json_build_object('type', 'FeatureCollection', 'features', json_agg(ST_AsGeoJSON(census_tracts.*)::json))
- from census_tracts
- """,
- (year,),
- )
- return cur.fetchall()
-
-
-@app.get("/high_frequency_transit_lines")
-async def read_census_tracts(year: int, db=Depends(get_db)):
- cur = db.cursor()
- cur.execute(
- """
- with census_tracts as (
- select census_tract, geom from api__census_tracts
- where year_ = %s
- )
- select json_build_object('type', 'FeatureCollection', 'features', json_agg(ST_AsGeoJSON(census_tracts.*)::json))
- from census_tracts
- """,
- (year,),
- )
- return cur.fetchall()
-
-
-# @app.get("/predict")
-# async def read_predict(
-# samples=100, predictor: TractsModelPredictor = Depends(get_predictor)
-# ):
-# result = predictor.predict(samples=samples)
-# return result.tolist()
diff --git a/api/postgrest.conf b/api/postgrest.conf
deleted file mode 100644
index ddb71965..00000000
--- a/api/postgrest.conf
+++ /dev/null
@@ -1,107 +0,0 @@
-## Admin server used for checks. It's disabled by default unless a port is specified.
-# admin-server-port = 3001
-
-## The database role to use when no client authentication is provided
-db-anon-role = "web_anon"
-
-## Notification channel for reloading the schema cache
-db-channel = "pgrst"
-
-## Enable or disable the notification channel
-db-channel-enabled = true
-
-## Enable in-database configuration
-db-config = true
-
-## Function for in-database configuration
-## db-pre-config = "postgrest.pre_config"
-
-## Extra schemas to add to the search_path of every request
-db-extra-search-path = "public"
-
-## Limit rows in response
-# db-max-rows = 1000
-
-## Allow getting the EXPLAIN plan through the `Accept: application/vnd.pgrst.plan` header
-# db-plan-enabled = false
-
-## Number of open connections in the pool
-db-pool = 10
-
-## Time in seconds to wait to acquire a slot from the connection pool
-# db-pool-acquisition-timeout = 10
-
-## Time in seconds after which to recycle pool connections
-# db-pool-max-lifetime = 1800
-
-## Time in seconds after which to recycle unused pool connections
-# db-pool-max-idletime = 30
-
-## Allow automatic database connection retrying
-# db-pool-automatic-recovery = true
-
-## Stored proc to exec immediately after auth
-# db-pre-request = "stored_proc_name"
-
-## Enable or disable prepared statements. disabling is only necessary when behind a connection pooler.
-## When disabled, statements will be parametrized but won't be prepared.
-db-prepared-statements = true
-
-## The name of which database schema to expose to REST clients
-db-schemas = "api"
-
-## How to terminate database transactions
-## Possible values are:
-## commit (default)
-## Transaction is always committed, this can not be overriden
-## commit-allow-override
-## Transaction is committed, but can be overriden with Prefer tx=rollback header
-## rollback
-## Transaction is always rolled back, this can not be overriden
-## rollback-allow-override
-## Transaction is rolled back, but can be overriden with Prefer tx=commit header
-db-tx-end = "commit"
-
-## The standard connection URI format, documented at
-## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
-db-uri = "postgresql://postgres@34.123.100.76:5432/cities"
-
-# jwt-aud = "your_audience_claim"
-
-## Jspath to the role claim key
-jwt-role-claim-key = ".role"
-
-## Choose a secret, JSON Web Key (or set) to enable JWT auth
-## (use "@filename" to load from separate file)
-# jwt-secret = "secret_with_at_least_32_characters"
-jwt-secret-is-base64 = false
-
-## Enables and set JWT Cache max lifetime, disables caching with 0
-# jwt-cache-max-lifetime = 0
-
-## Logging level, the admitted values are: crit, error, warn, info and debug.
-log-level = "error"
-
-## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
-## Admitted values: follow-privileges, ignore-privileges, disabled
-openapi-mode = "follow-privileges"
-
-## Base url for the OpenAPI output
-openapi-server-proxy-uri = ""
-
-## Configurable CORS origins
-# server-cors-allowed-origins = ""
-
-server-host = "!4"
-server-port = 3001
-
-## Allow getting the request-response timing information through the `Server-Timing` header
-server-timing-enabled = true
-
-## Unix socket location
-## if specified it takes precedence over server-port
-# server-unix-socket = "/tmp/pgrst.sock"
-
-## Unix socket file mode
-## When none is provided, 660 is applied by default
-# server-unix-socket-mode = "660"
diff --git a/api/schema.sql b/api/schema.sql
deleted file mode 100644
index 2285c2b7..00000000
--- a/api/schema.sql
+++ /dev/null
@@ -1,67 +0,0 @@
-begin;
-drop schema if exists api cascade;
-
-create schema api;
-
-create view api.demographics as (
- select * from api__demographics
-);
-
-create view api.census_tracts as (
- select * from api__census_tracts
-);
-
-create function api.high_frequency_transit_lines() returns setof dev.api__high_frequency_transit_lines as $$
- select * from dev.api__high_frequency_transit_lines
-$$ language sql;
-
-create function api.high_frequency_transit_lines(
- blue_zone_radius double precision,
- yellow_zone_line_radius double precision,
- yellow_zone_stop_radius double precision
-) returns table (
- valid daterange,
- geom geometry(LineString, 4269),
- blue_zone_geom geometry(LineString, 4269),
- yellow_zone_geom geometry(Geometry, 4269)
-) as $$
- with
- lines as (select * from dev.stg_high_frequency_transit_lines_union),
- stops as (select * from dev.high_frequency_transit_stops),
- lines_and_stops as (
- select
- lines.valid * stops.valid as valid,
- lines.geom as line_geom,
- stops.geom as stop_geom
- from lines inner join stops on lines.valid && stops.valid
- )
- select
- valid,
- st_transform(line_geom, 4269) as geom,
- st_transform(st_buffer(line_geom, blue_zone_radius), 4269) as blue_zone_geom,
- st_transform(st_union(st_buffer(line_geom, yellow_zone_line_radius), st_buffer(stop_geom, yellow_zone_stop_radius)), 4269) as yellow_zone_geom
- from lines_and_stops
-$$ language sql;
-
-do $$
-begin
-create role web_anon nologin;
-exception when duplicate_object then raise notice '%, skipping', sqlerrm using errcode = sqlstate;
-end
-$$;
-
-grant all on schema public to web_anon;
-grant all on schema dev to web_anon;
-grant select on table public.spatial_ref_sys TO web_anon;
-grant usage on schema api to web_anon;
-grant all on all tables in schema api to web_anon;
-grant all on all functions in schema api to web_anon;
-grant all on schema api to web_anon;
-GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA dev TO web_anon;
-GRANT ALL PRIVILEGES ON ALL functions IN SCHEMA dev TO web_anon;
-GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA api TO web_anon;
-GRANT ALL PRIVILEGES ON ALL functions IN SCHEMA api TO web_anon;
-GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO web_anon;
-GRANT ALL PRIVILEGES ON ALL functions IN SCHEMA public TO web_anon;
-grant web_anon to postgres;
-commit;
From 8a76e97fe39d9d160b059d38487d69dec547e1c8 Mon Sep 17 00:00:00 2001
From: Jack Feser
Date: Fri, 6 Sep 2024 14:47:14 -0400
Subject: [PATCH 137/142] select correct geometries
---
dbt/models/staging/stg_high_frequency_transit_lines_union.sql | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dbt/models/staging/stg_high_frequency_transit_lines_union.sql b/dbt/models/staging/stg_high_frequency_transit_lines_union.sql
index 56d4b680..4de6bbdb 100644
--- a/dbt/models/staging/stg_high_frequency_transit_lines_union.sql
+++ b/dbt/models/staging/stg_high_frequency_transit_lines_union.sql
@@ -4,14 +4,14 @@ lines_2015 as (
st_union(st_transform(geom, {{ var("srid") }})) as geom
from
{{ source('minneapolis', 'high_frequency_transit_2015_freq_lines') }}
- where st_geometrytype(geom) = 'ST_LineString'
+ where st_geometrytype(geom) = 'ST_MultiLineString'
),
lines_2016 as (
select
st_union(st_transform(geom, {{ var("srid") }})) as geom
from
{{ source('minneapolis', 'high_frequency_transit_2016_freq_lines') }}
- where st_geometrytype(geom) = 'ST_LineString'
+ where st_geometrytype(geom) = 'ST_MultiLineString'
)
select
'(,2016-01-01)'::daterange as valid,
From d37849a7da698c6d320bfaa3fc173f5526deab2b Mon Sep 17 00:00:00 2001
From: rfl-urbaniak
Date: Mon, 9 Sep 2024 06:22:54 -0400
Subject: [PATCH 138/142] update .gitignore from ru-tracts-model
---
.gitignore | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/.gitignore b/.gitignore
index afc863d2..bbeb945f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,18 @@ tests/.coverage
*.DS_Store
.vscode/launch.json
+data/sql/counties_database.db
+data/sql/msa_database.db
+.Rproj.user
+**/*.RData
+**/*.Rhistory
+
+# data
+data/minneapolis/processed/values_long.csv
+data/minneapolis/processed/values_with_parking.csv
+data/minneapolis/sourced/demographic/**
+data/minneapolis/preds/**
+data/minneapolis/sourced/parcel_to_census_tract_mappings/**
+data/minneapolis/sourced/parcel_to_parking_info_mappings/**
+
+data/minneapolis/.pgpass
From 6e2a361c71e00201b4546245f82cf20ca1c3a661 Mon Sep 17 00:00:00 2001
From: rfl-urbaniak
Date: Mon, 9 Sep 2024 06:33:48 -0400
Subject: [PATCH 139/142] new format lint
---
cities/modeling/model_interactions.py | 2 +-
cities/modeling/modeling_utils.py | 2 +-
cities/queries/causal_insight.py | 5 +++--
scripts/clean.sh | 22 ++++------------------
scripts/lint.sh | 14 +++++---------
5 files changed, 14 insertions(+), 31 deletions(-)
diff --git a/cities/modeling/model_interactions.py b/cities/modeling/model_interactions.py
index 8232410f..2446d6d5 100644
--- a/cities/modeling/model_interactions.py
+++ b/cities/modeling/model_interactions.py
@@ -3,10 +3,10 @@
from typing import Optional
import dill
+import pyro
import pyro.distributions as dist
import torch
-import pyro
from cities.modeling.modeling_utils import (
prep_wide_data_for_inference,
train_interactions_model,
diff --git a/cities/modeling/modeling_utils.py b/cities/modeling/modeling_utils.py
index 966a0ba5..55aaccc6 100644
--- a/cities/modeling/modeling_utils.py
+++ b/cities/modeling/modeling_utils.py
@@ -2,13 +2,13 @@
import matplotlib.pyplot as plt
import pandas as pd
+import pyro
import torch
from pyro.infer import SVI, Trace_ELBO
from pyro.infer.autoguide import AutoNormal
from pyro.optim import Adam # type: ignore
from scipy.stats import spearmanr
-import pyro
from cities.utils.data_grabber import (
DataGrabber,
list_available_features,
diff --git a/cities/queries/causal_insight.py b/cities/queries/causal_insight.py
index 187855ea..7a7a7e98 100644
--- a/cities/queries/causal_insight.py
+++ b/cities/queries/causal_insight.py
@@ -5,10 +5,10 @@
import numpy as np
import pandas as pd
import plotly.graph_objects as go
+import pyro
import torch
from sklearn.preprocessing import StandardScaler
-import pyro
from cities.modeling.model_interactions import model_cities_interaction
from cities.modeling.modeling_utils import prep_wide_data_for_inference
from cities.utils.cleaning_utils import (
@@ -576,7 +576,8 @@ def estimate_ATE(self):
label=f"mean = {tau_samples.mean():.3f}",
)
plt.title(
- f"ATE for {self.intervention_dataset} and {self.outcome_dataset} with forward shift = {self.forward_shift}"
+ f"ATE for {self.intervention_dataset} and {self.outcome_dataset} "
+ f"with forward shift = {self.forward_shift}"
)
plt.ylabel("counts")
plt.xlabel("ATE")
diff --git a/scripts/clean.sh b/scripts/clean.sh
index 6918545f..898f2e55 100755
--- a/scripts/clean.sh
+++ b/scripts/clean.sh
@@ -1,22 +1,8 @@
#!/bin/bash
set -euxo pipefail
-# isort suspended as conflicting with black
-# nbqa isort docs/guides/
-
-
-# this sometimes conflicts with black but does some
-# preliminary import sorting
-# and is then overriden by black
-isort cities/ tests/
-
-black ./cities/ ./tests/ ./docs/guides/
-
-black docs/guides/
-
+isort --profile="black" cities/ tests/
autoflake --remove-all-unused-imports --in-place --recursive ./cities ./tests
-
-nbqa autoflake --nbqa-shell --remove-all-unused-imports --recursive --in-place docs/guides/
-
-#nbqa black docs/guides/
-
+nbqa --nbqa-shell isort --profile="black" docs/guides/
+nbqa --nbqa-shell autoflake --nbqa-shell --remove-all-unused-imports --recursive --in-place docs/guides/
+black ./cities ./tests docs/guides/
diff --git a/scripts/lint.sh b/scripts/lint.sh
index 538aeeb1..2015aa76 100755
--- a/scripts/lint.sh
+++ b/scripts/lint.sh
@@ -1,12 +1,8 @@
#!/bin/bash
set -euxo pipefail
-mypy --ignore-missing-imports cities/
-#isort --check --diff cities/ tests/
-black --check cities/ tests/
-flake8 cities/ tests/ --ignore=E203,W503 --max-line-length=127
-
-
-nbqa autoflake -v --recursive --check docs/guides/
-#nbqa isort --check docs/guides/
-nbqa black --check docs/guides/
+mypy --ignore-missing-imports cities/ tests/
+isort --check --profile="black" --diff cities/ tests/
+black --check cities/ tests/ docs/guides/
+flake8 cities/ tests/
+nbqa --nbqa-shell autoflake --nbqa-shell --recursive --check docs/guides/
From 5cf1ca579e55fd8698ec289e723ec5b563106d6b Mon Sep 17 00:00:00 2001
From: rfl-urbaniak
Date: Mon, 9 Sep 2024 06:53:45 -0400
Subject: [PATCH 140/142] pin pyro to pass inference test
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 3f14029a..1b6a832d 100644
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,7 @@
]
DEV_REQUIRES = [
- "pyro-ppl>=1.8.5",
+ "pyro-ppl=1.8.5",
"torch",
"plotly.express",
"scipy",
From e79617c50e1203d68bf2ac1a40a393e81cd27a55 Mon Sep 17 00:00:00 2001
From: rfl-urbaniak
Date: Mon, 9 Sep 2024 07:02:46 -0400
Subject: [PATCH 141/142] getting inference test to work
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 1b6a832d..6d4e13aa 100644
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,7 @@
]
DEV_REQUIRES = [
- "pyro-ppl=1.8.5",
+ "pyro-ppl==1.8.5",
"torch",
"plotly.express",
"scipy",
From 348bc0086e7d27c0579e6a906418793ecda3a327 Mon Sep 17 00:00:00 2001
From: rfl-urbaniak
Date: Mon, 9 Sep 2024 07:46:29 -0400
Subject: [PATCH 142/142] add seaborn to setup to pass notebook tests
---
docs/guides/counterfactual-explained.ipynb | 14 +++++++-------
setup.py | 1 +
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/docs/guides/counterfactual-explained.ipynb b/docs/guides/counterfactual-explained.ipynb
index 1f2bcd99..7f1f65da 100644
--- a/docs/guides/counterfactual-explained.ipynb
+++ b/docs/guides/counterfactual-explained.ipynb
@@ -741,7 +741,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -5895,7 +5895,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -5907,7 +5907,7 @@
},
{
"data": {
- "image/png": "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DlJzh0JERET/YOJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCArcwfwsOTkZCxZsgRxcXFISEiAn58ftmzZott/+/ZtdOvWrdxzra2tcf78+QrLjo2NxdixY8ts79OnD77++uvqB09ERETPPItKnBISEnDw4EGEh4dDq9VCEAS9/R4eHlizZo3eNkEQMGnSJLRt29aga3z++efw8/PTPXdxcal+4ERERFQrWFTiFB0dje7duwMA5syZgwsXLujtt7a2RkREhN622NhY5OXloV+/fgZdo3HjxggNDTVJvERERFS7WNQYJ7G46uFs2bIF9vb2iI6OfgIREREREZWyqBanqlKr1di1axdiYmIgk8kMOuell15CVlYW3N3d0bdvX8yaNQtyubxacQiCgIKCgmqV8SiFQqH3b23H+ijFutDH+ijFutDH+ijFutBXnXqo0YnToUOHkJWVZVA3nYODAyZNmoRWrVpBJpPhxIkTWLp0KRITE7Fo0aJqxaFWqxEfH1+tMiqSlJT0RMqtqVgfpVgX+lgfpVgX+lgfpVgX1VejE6fNmzejTp06iIqKeuyxISEhCAkJ0T2PioqCh4cHPvroI5w7dw5hYWFGxyGVShEQEGD0+eVRKBRISkqCr68vbGxsTFp2TcT6KMW60Mf6KMW60Mf6KMW60FdSH8aosYlTfn4+9u/fj6FDh0IikRhVRu/evfHRRx/hwoUL1UqcRCIRbG1tjT6/MjY2Nk+s7JqI9VGKdaGP9VGKdaGP9VGKdVF9FjU4vCp2794NpVKJ/v37mzsUIiIiqiVqbOK0ZcsWNGjQAOHh4UaXsXXrVgDg9ARERERkEIvqqlMoFDh48CAAICUlBXl5edixYwcAoHXr1nB1dQUAZGRk4Pjx45g8eXK55aSkpCAmJgavvPIKpk+fDgCYPXs2GjZsiJCQEN3g8F9++QXdu3dn4kREREQGsajEKT09HbNmzdLbVvJ8+fLlaNOmDQBg+/btKCoqqrCbThAEaDQavZnHGzdujM2bN2Pp0qVQq9WoX78+pk6dipdeeukJvRoiIiJ61lhU4uTt7Y0rV6489rhRo0Zh1KhR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RERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAYSCYIgmDuImuyvv/6CIAiwtrY2abmCIECtVkMqlUIkEpm07JqI9VGKdaGP9VGKdaGP9VGKdaGvpD5EIhFatGhRpXOtnlBMtcaT+gCKRCKTJ2M1GeujFOtCH+ujFOtCH+ujFOtCn0gk0j2qfC5bnIiIiIgMwzFORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZOFub69et48cUXERERgfbt2+M///kPCgsLzR2WWSQnJ+P999/HgAEDEBISgn79+pk7JLPZvn07Xn75ZXTq1AkREREYMGAAfv/9d9TWNboPHjyI0aNHo23btmjWrBm6deuGzz//HLm5ueYOzezy8/PRqVMnBAUF4fz58+YO56nbsGEDgoKCyjz+97//mTs0s9m4cSMGDhyI0NBQtGnTBpMmTYJSqTR3WE/dmDFjyv1sBAUFYevWrQaXY/UEY6Qqys7Oxrhx4+Dr64v58+cjNTUVX3zxBZRKJd5//31zh/fUJSQk4ODBgwgPD4dWq621SQIA/PLLL6hfvz7mzJkDFxcXHDt2DO+99x7u3buH6dOnmzu8py4rKwthYWEYM2YMnJ2dkZCQgPnz5yMhIQFLly41d3hmtXDhQmg0GnOHYXY//fQTHBwcdM89PT3NGI35fP/991i8eDGmTp2KiIgIZGZm4vjx47XyM/LBBx8gLy9Pb9uyZcuwa9cuREVFGV6QQBbjhx9+ECIiIoTMzEzdtt9++00IDg4W7t27Z77AzESj0ej+/9Zbbwl9+/Y1YzTmlZ6eXmbbu+++K7Ro0UKvnmqzNWvWCIGBgbXyZ6XEtWvXhIiICGH16tVCYGCgcO7cOXOH9NStX79eCAwMLPdnpra5fv26EBISIhw4cMDcoVis6OhoYfLkyVU6h111FuTQoUOIioqCs7Ozblvv3r2h1Wpx9OhR8wVmJmIxP54lXF1dy2wLDg5GXl4eCgoKzBCR5Sn5uVGr1eYNxIw++eQTvPDCC2jUqJG5QyELsGHDBnh7e6Nz587mDsUi/fXXX7h9+zb69+9fpfP4l8mCJCYm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",
+ "image/png": "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DlJzh0JERET/YOJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCAmTkREREQGYuJEREREZCArcwfwsOTkZCxZsgRxcXFISEiAn58ftmzZott/+/ZtdOvWrdxzra2tcf78+QrLjo2NxdixY8ts79OnD77++uvqB09ERETPPItKnBISEnDw4EGEh4dDq9VCEAS9/R4eHlizZo3eNkEQMGnSJLRt29aga3z++efw8/PTPXdxcal+4ERERFQrWFTiFB0dje7duwMA5syZgwsXLujtt7a2RkREhN622NhY5OXloV+/fgZdo3HjxggNDTVJvERERFS7WNQYJ7G46uFs2bIF9vb2iI6OfgIREREREZWyqBanqlKr1di1axdiYmIgk8kMOuell15CVlYW3N3d0bdvX8yaNQtyubxacQiCgIKCgmqV8SiFQqH3b23H+ijFutDH+ijFutDH+ijFutBXnXqo0YnToUOHkJWVZVA3nYODAyZNmoRWrVpBJpPhxIkTWLp0KRITE7Fo0aJqxaFWqxEfH1+tMiqSlJT0RMqtqVgfpVgX+lgfpVgX+lgfpVgX1VejE6fNmzejTp06iIqKeuyxISEhCAkJ0T2PioqCh4cHPvroI5w7dw5hYWFGxyGVShEQEGD0+eVRKBRISkqCr68vbGxsTFp2TcT6KMW60Mf6KMW60Mf6KMW60FdSH8aosYlTfn4+9u/fj6FDh0IikRhVRu/evfHRRx/hwoUL1UqcRCIRbG1tjT6/MjY2Nk+s7JqI9VGKdaGP9VGKdaGP9VGKdVF9FjU4vCp2794NpVKJ/v37mzsUIiIiqiVqbOK0ZcsWNGjQAOHh4UaXsXXrVgDg9ARERERkEIvqqlMoFDh48CAAICUlBXl5edixYwcAoHXr1nB1dQUAZGRk4Pjx45g8eXK55aSkpCAmJgavvPIKpk+fDgCYPXs2GjZsiJCQEN3g8F9++QXdu3dn4kREREQGsajEKT09HbNmzdLbVvJ8+fLlaNOmDQBg+/btKCoqqrCbThAEaDQavZnHGzdujM2bN2Pp0qVQq9WoX78+pk6dipdeeukJvRoiIiJ61lhU4uTt7Y0rV6489rhRo0Zh1KhR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RERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAYSCYIgmDuImuyvv/6CIAiwtrY2abmCIECtVkMqlUIkEpm07JqI9VGKdaGP9VGKdaGP9VGKdaGvpD5EIhFatGhRpXOtnlBMtcaT+gCKRCKTJ2M1GeujFOtCH+ujFOtCH+ujFOtCn0gk0j2qfC5bnIiIiIgMwzFORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZORERERAZi4kRERERkICZOFub69et48cUXERERgfbt2+M///kPCgsLzR2WWSQnJ+P999/HgAEDEBISgn79+pk7JLPZvn07Xn75ZXTq1AkREREYMGAAfv/9d9TWNboPHjyI0aNHo23btmjWrBm6deuGzz//HLm5ueYOzezy8/PRqVMnBAUF4fz58+YO56nbsGEDgoKCyjz+97//mTs0s9m4cSMGDhyI0NBQtGnTBpMmTYJSqTR3WE/dmDFjyv1sBAUFYevWrQaXY/UEY6Qqys7Oxrhx4+Dr64v58+cjNTUVX3zxBZRKJd5//31zh/fUJSQk4ODBgwgPD4dWq621SQIA/PLLL6hfvz7mzJkDFxcXHDt2DO+99x7u3buH6dOnmzu8py4rKwthYWEYM2YMnJ2dkZCQgPnz5yMhIQFLly41d3hmtXDhQmg0GnOHYXY//fQTHBwcdM89PT3NGI35fP/991i8eDGmTp2KiIgIZGZm4vjx47XyM/LBBx8gLy9Pb9uyZcuwa9cuREVFGV6QQBbjhx9+ECIiIoTMzEzdtt9++00IDg4W7t27Z77AzESj0ej+/9Zbbwl9+/Y1YzTmlZ6eXmbbu+++K7Ro0UKvnmqzNWvWCIGBgbXyZ6XEtWvXhIiICGH16tVCYGCgcO7cOXOH9NStX79eCAwMLPdnpra5fv26EBISIhw4cMDcoVis6OhoYfLkyVU6h111FuTQoUOIioqCs7Ozblvv3r2h1Wpx9OhR8wVmJmIxP54lXF1dy2wLDg5GXl4eCgoKzBCR5Sn5uVGr1eYNxIw++eQTvPDCC2jUqJG5QyELsGHDBnh7e6Nz587mDsUi/fXXX7h9+zb69+9fpfP4l8mCJCYm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",
"text/plain": [
""
]
@@ -5962,7 +5962,7 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 6,
"metadata": {},
"outputs": [
{
@@ -6001,12 +6001,12 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
- "image/png": "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7IwGDRq89HmJiIwJRzSIyGgdOXIE165dg1qtxv379xEXF4c///wTlStXxpIlS4pNap42bRr69euHDz74AH369EG1atVw//59nD17FqmpqU/suVFYWIiAgAB06tQJ169fx/r169G8eXO0a9cOwKM5B9OnT0dISAh69eqFzp07w9nZGbdv30ZMTAyaNWuGqVOnvtLz8fX1RbNmzfDDDz8gJSUFtWvXRkRExFP30XjV5/MidnZ2mDRpEiZPnoyPPvoIXbt2RZkyZXDp0iUolcpicyfMzc3RpUsXrF27FqampujSpctLnaNFixaYMmUKZs6ciY4dOxbtDF5QUIAbN25g165dMDc3R7ly5V54rKZNm8LBwQETJ06Ev78/ZDIZduzY8cyyUqFCBaxYsQIpKSl46623sHfvXiQmJmLGjBnFJv0TEdH/Y9EgIqO1aNEiAI8++Do6OsLNzQ1ffvklevXq9cRk7Nq1a2PLli1YvHgxtm3bhszMTDg7O6N+/foICgp64thTp07Frl27sGjRIhQWFqJLly6YPHlysQnPH3zwASpUqIDly5fj119/RUFBAVxcXNCiRYunrpL0IiYmJliyZEnRZGuZTAZfX19MnDgRPXr0eKPn8zJ69+6NsmXLYvny5QgNDYWZmRlq1qyJgICAJ+7bvXt3rF27Fi1btkSFChVe+hz9+vVD06ZNER4ejv379yMtLQ3m5uaoVq0aevbsib59+77UfBknJycsXboUc+bMwYIFC1CmTBl069YNLVu2xJAhQ564v4ODA7777jvMnDkTmzZtQrly5TB16lT06dPnpbMTERkbmcTZaUREWrN161ZMmjQJmzdvxjvvvCM6js66dOkSunfvjjlz5jxRgnSNv78/MjIysHv3btFRiIj0CudoEBFRqdu0aRNsbGzQoUMH0VGIiKiE8NIpIiIqNVFRUbhy5Qo2bdqE/v37P3UDPiIiMgwsGkREVGpmzpyJ+/fvw9vbG6NGjRIdh4iIShDnaBARERERkdZxjgYREREREWkdiwYREREREWkdiwYREREREWkdiwYREREREWkdiwYREREREWkdiwYREREREWkdiwYREREREWkdiwYREREREWkdiwYREREREWmdmegARERERCUtX6VGZr4KCpUaSpUaSpUGCpUaBWoNNBIgSRJkMhlMZICFqQmszUxhZWYCKzNTWJuZ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",
+ "image/png": "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",
"text/plain": [
""
]
@@ -6061,7 +6061,7 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 8,
"metadata": {},
"outputs": [
{
diff --git a/setup.py b/setup.py
index 6d4e13aa..fc86c68a 100644
--- a/setup.py
+++ b/setup.py
@@ -48,6 +48,7 @@
"dill",
"plotly",
"matplotlib>=3.8.2",
+ "seaborn",
],
extras_require={"test": TEST_REQUIRES, "dev": DEV_REQUIRES + TEST_REQUIRES},
python_requires=">=3.10",