From 24983e71b161d773781e805ec25460a4ef41eb26 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Tue, 30 Sep 2025 13:57:34 -0500 Subject: [PATCH 1/6] Update README to rename repo and reference latest development/deployment instructions --- README.md | 86 ++++++++++++++++++++++++++----------------------------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 27d52467..ff7b7be6 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,12 @@ -# PINVAL +# HomeVal This repo stores the code necessary to generate experimental reports explaining how the CCAO Data team's residential model valued any particular single-family home or multifamily home with six or fewer units. -> [!WARNING] -> This project is an experimental work-in-progress and is not yet used in -> production. Reports generated using this code may not accurately reflect -> model behavior or CCAO policy. +## Development -## Developing - -We currently support two ways of generating PINVAL reports: With -[Quarto](#quarto) or with [Hugo](#hugo). - -### Quarto - -This project expects that you have R and the [Quarto -CLI](https://quarto.org/docs/get-started/) installed on your machine. -A working installation of RStudio is recommended, but not required. - -1. Ensure that renv is installed: - -```r -install.packages("renv") -``` - -2. Create a renv environment and install R dependencies: - -```r -renv::restore() -``` - -3. [Optional] If you would like to run the report for a specific PIN, year, or - model run, adjust run parameters under the `params` attribute in the YAML - front matter in `pinval.qmd`. - -4. Make sure you are [authenticated with - AWS](https://github.com/ccao-data/wiki/blob/master/How-To/Setup-the-AWS-Command-Line-Interface-and-Multi-factor-Authentication.md). - -5. Render the `pinval.qmd` report using Quarto, either by clicking the "Render" - button in the RStudio UI or by calling the Quarto CLI: - -``` -quarto render pinval.qmd --to html -o pinval.html -``` - -### Hugo +### Running a development server This project expects that you have the [Hugo CLI](https://gohugo.io/installation/) installed on your machine. @@ -68,6 +28,40 @@ cd hugo hugo serve ``` -3. For a quick sample report, see the examples: - - http://localhost:1313/example-single-card - - http://localhost:1313/example-multi-card +3. Any reports that [you have + generated](https://github.com/ccao-data/homeval/blob/main/scripts/generate_pinval/generate_pinval.py) + will be available in a browser at http://localhost:1313/. + +### Generating reports + +You can use the [`scripts/generate_homeval` +script](https://github.com/ccao-data/homeval/blob/main/scripts/generate_homeval/generate_homeval.py) +to generate reports that you can view locally. + +Start by creating a virtual environment for the script and installing its +dependencies: + +``` +uv venv scripts/generate_homeval/.venv +source scripts/generate_homeval/.venv/bin/activate +uv pip install scripts/generate_homeval +``` + +Then, use the script to generate reports for one or more PINs using a given +comps run ID: + +``` +AWS_ATHENA_S3_STAGING_DIR="" python3 scripts/generate_homeval/generate_homeval.py \ + --run-id \ + --pin +``` + +Run `python3 scripts/generate_homeval/generate_homeval.py --help` to see full +documentation for the script. + +## Deployment + +We manage deployments using the [`generate-homeval` GitHub +workflow](https://github.com/ccao-data/homeval/actions/workflows/generate-pinval.yaml). +That workflow includes options for deploying to a development environment as +well as the production environment. From 5ec4bf68c3b61ba81695ba91c32cf864997cf1d6 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Tue, 30 Sep 2025 13:58:57 -0500 Subject: [PATCH 2/6] Rename generate_pinval script to generate_homeval --- .../.python-version | 0 .../constants.py | 6 ++--- .../generate_homeval.py} | 22 +++++++++---------- .../pyproject.toml | 2 +- .../resolve_model_metadata.py | 8 +++---- .../uv.lock | 0 6 files changed, 19 insertions(+), 19 deletions(-) rename scripts/{generate_pinval => generate_homeval}/.python-version (100%) rename scripts/{generate_pinval => generate_homeval}/constants.py (56%) rename scripts/{generate_pinval/generate_pinval.py => generate_homeval/generate_homeval.py} (98%) rename scripts/{generate_pinval => generate_homeval}/pyproject.toml (96%) rename scripts/{generate_pinval => generate_homeval}/resolve_model_metadata.py (97%) rename scripts/{generate_pinval => generate_homeval}/uv.lock (100%) diff --git a/scripts/generate_pinval/.python-version b/scripts/generate_homeval/.python-version similarity index 100% rename from scripts/generate_pinval/.python-version rename to scripts/generate_homeval/.python-version diff --git a/scripts/generate_pinval/constants.py b/scripts/generate_homeval/constants.py similarity index 56% rename from scripts/generate_pinval/constants.py rename to scripts/generate_homeval/constants.py index 4c38f350..61e6689a 100644 --- a/scripts/generate_pinval/constants.py +++ b/scripts/generate_homeval/constants.py @@ -2,6 +2,6 @@ # It's helpful to factor these tables out into shared constants because we often # need to switch to dev tables for testing -PINVAL_ASSESSMENT_CARD_TABLE = "pinval.vw_assessment_card" -PINVAL_COMP_TABLE = "pinval.vw_comp" -PINVAL_DATA_DICT_TABLE = "pinval.vars_dict" +HOMEVAL_ASSESSMENT_CARD_TABLE = "pinval.vw_assessment_card" +HOMEVAL_COMP_TABLE = "pinval.vw_comp" +HOMEVAL_DATA_DICT_TABLE = "pinval.vars_dict" diff --git a/scripts/generate_pinval/generate_pinval.py b/scripts/generate_homeval/generate_homeval.py similarity index 98% rename from scripts/generate_pinval/generate_pinval.py rename to scripts/generate_homeval/generate_homeval.py index 1df13469..868b5d26 100644 --- a/scripts/generate_pinval/generate_pinval.py +++ b/scripts/generate_homeval/generate_homeval.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Generate PINVAL report markdown (and optionally HTML) files for a given comps +Generate HomeVal report markdown (and optionally HTML) files for a given comps model run ID. A user may ask for either **one or more explicit PINs** *or* for **all PINs that belong to a triad** (city, north, south). Exactly one of the two must be supplied. If the user passes an empty string for either of the --pin or --triad @@ -9,16 +9,16 @@ Examples -------- Generate every PIN: - $ python3 generate_pinval.py \ + $ python3 generate_homeval.py \ --run-id 2025-04-25-fancy-free-billy \ Generate two specific PINs: - $ python3 generate_pinval.py \ + $ python3 generate_homeval.py \ --run-id 2025-04-25-fancy-free-billy \ --pin 01011000040000 10112040080000 Generate every PIN in towns 10 and 11: - $ python3 generate_pinval.py \ + $ python3 generate_homeval.py \ --run-id 2025-04-25-fancy-free-billy \ --township 10 11 """ @@ -52,7 +52,7 @@ def parse_args() -> argparse.Namespace: """Parse command‑line arguments and perform basic validation.""" parser = argparse.ArgumentParser( - description="Generate PINVAL report markdown (and optionally HTML) files", + description="Generate HomeVal report markdown (and optionally HTML) files", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) @@ -660,7 +660,7 @@ def load_vars_dict(cursor) -> dict[str, dict]: """ Pull the crosswalk for labels and tooltips into a dict: """ - sql = f"""SELECT code, display_name, description FROM {constants.PINVAL_DATA_DICT_TABLE}""" + sql = f"""SELECT code, display_name, description FROM {constants.HOMEVAL_DATA_DICT_TABLE}""" df = run_athena_query(cursor, sql) # Return a dict where each key is a column code and the value is a dict with label and description info return { @@ -731,7 +731,7 @@ def main() -> None: assessment_sql = f""" SELECT * - FROM {constants.PINVAL_ASSESSMENT_CARD_TABLE} + FROM {constants.HOMEVAL_ASSESSMENT_CARD_TABLE} WHERE {where_assessment} """ @@ -752,10 +752,10 @@ def main() -> None: comps_sql = f""" SELECT comp.* - FROM {constants.PINVAL_COMP_TABLE} AS comp + FROM {constants.HOMEVAL_COMP_TABLE} AS comp INNER JOIN ( SELECT DISTINCT meta_pin - FROM {constants.PINVAL_ASSESSMENT_CARD_TABLE} + FROM {constants.HOMEVAL_ASSESSMENT_CARD_TABLE} WHERE {where_assessment} ) AS card ON comp.pin = card.meta_pin @@ -789,7 +789,7 @@ def main() -> None: # Declare outputs paths hugo_root = project_root / "hugo" content_root = hugo_root / "content" - md_outdir = content_root / "pinval-reports" + md_outdir = content_root / "homeval-reports" md_outdir.mkdir(parents=True, exist_ok=True) start_time_dict_groupby = time.time() @@ -868,7 +868,7 @@ def main() -> None: # Move reports directory out of the Hugo content root so that we # can move reports back in neighborhood-by-neighborhood - temp_reports_dir = hugo_root / "temp-pinval-reports" + temp_reports_dir = hugo_root / "temp-homeval-reports" if temp_reports_dir.exists(): shutil.rmtree(temp_reports_dir) shutil.move(str(md_outdir), str(temp_reports_dir)) diff --git a/scripts/generate_pinval/pyproject.toml b/scripts/generate_homeval/pyproject.toml similarity index 96% rename from scripts/generate_pinval/pyproject.toml rename to scripts/generate_homeval/pyproject.toml index dcf46b9b..22dc2007 100644 --- a/scripts/generate_pinval/pyproject.toml +++ b/scripts/generate_homeval/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "generate-pinval" +name = "generate-homeval" version = "0.1.0" requires-python = ">=3.11" dependencies = [ diff --git a/scripts/generate_pinval/resolve_model_metadata.py b/scripts/generate_homeval/resolve_model_metadata.py similarity index 97% rename from scripts/generate_pinval/resolve_model_metadata.py rename to scripts/generate_homeval/resolve_model_metadata.py index 08a8e93f..e97a7909 100644 --- a/scripts/generate_pinval/resolve_model_metadata.py +++ b/scripts/generate_homeval/resolve_model_metadata.py @@ -129,7 +129,7 @@ def get_township_codes( .execute( f""" SELECT DISTINCT meta_township_code - FROM {constants.PINVAL_ASSESSMENT_CARD_TABLE} + FROM {constants.HOMEVAL_ASSESSMENT_CARD_TABLE} WHERE assessment_year = %(year)s {"AND is_report_eligible" if eligible_only else ""} ORDER BY meta_township_code @@ -160,7 +160,7 @@ def get_run_id(run_id: str, year: str) -> str: .execute( f""" SELECT 1 - FROM {constants.PINVAL_COMP_TABLE} + FROM {constants.HOMEVAL_COMP_TABLE} WHERE run_id = %(run_id)s LIMIT 1 """, @@ -171,7 +171,7 @@ def get_run_id(run_id: str, year: str) -> str: if not run_id_is_valid: raise ValueError( f"Run ID {run_id} not found in view " - f"{constants.PINVAL_COMP_TABLE}. " + f"{constants.HOMEVAL_COMP_TABLE}. " "Double-check to make sure this is a comps run ID." ) else: @@ -186,7 +186,7 @@ def get_run_id(run_id: str, year: str) -> str: .execute( f""" SELECT MAX(run_id) - FROM {constants.PINVAL_COMP_TABLE} + FROM {constants.HOMEVAL_COMP_TABLE} WHERE assessment_year = %(year)s """, {"year": year}, diff --git a/scripts/generate_pinval/uv.lock b/scripts/generate_homeval/uv.lock similarity index 100% rename from scripts/generate_pinval/uv.lock rename to scripts/generate_homeval/uv.lock From f4c2e04261ca9c824b09193818f75b4c95761a22 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Tue, 30 Sep 2025 14:01:49 -0500 Subject: [PATCH 3/6] Rename generate-pinval workflow to generate-homeval --- ...rate-pinval.yaml => generate-homeval.yaml} | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) rename .github/workflows/{generate-pinval.yaml => generate-homeval.yaml} (93%) diff --git a/.github/workflows/generate-pinval.yaml b/.github/workflows/generate-homeval.yaml similarity index 93% rename from .github/workflows/generate-pinval.yaml rename to .github/workflows/generate-homeval.yaml index 4d7f223e..dbae6d8f 100644 --- a/.github/workflows/generate-pinval.yaml +++ b/.github/workflows/generate-homeval.yaml @@ -1,4 +1,4 @@ -name: generate-pinval +name: generate-homeval on: workflow_dispatch: @@ -109,16 +109,16 @@ jobs: uses: astral-sh/setup-uv@v5 with: enable-cache: true - cache-dependency-glob: scripts/generate_pinval/uv.lock + cache-dependency-glob: scripts/generate_homeval/uv.lock - name: Set up Python uses: actions/setup-python@v5 with: - python-version-file: scripts/generate_pinval/.python-version + python-version-file: scripts/generate_homeval/.python-version - name: Install Python requirements run: uv pip install . - working-directory: scripts/generate_pinval + working-directory: scripts/generate_homeval - name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@v4 @@ -138,7 +138,7 @@ jobs: if [ "${{ inputs.eligible-only }}" = "true" ]; then ELIG_FLAG="--eligible-only" fi - python3 scripts/generate_pinval/resolve_model_metadata.py \ + python3 scripts/generate_homeval/resolve_model_metadata.py \ --year "${{ inputs.year }}" \ --run-id "${{ inputs.run_id }}" \ --pin "${PINS[@]}" \ @@ -166,9 +166,9 @@ jobs: echo "$DISTRIBUTION_ID_STR" >> "$GITHUB_OUTPUT" ############################################################################### -# Generate PINVAL reports for a triad or explicit PINs +# Generate HomeVal reports for a triad or explicit PINs ############################################################################### - generate-pinval: + generate-homeval: needs: setup runs-on: ubuntu-latest strategy: @@ -190,16 +190,16 @@ jobs: uses: astral-sh/setup-uv@v5 with: enable-cache: true - cache-dependency-glob: scripts/generate_pinval/uv.lock + cache-dependency-glob: scripts/generate_homeval/uv.lock - name: Set up Python uses: actions/setup-python@v5 with: - python-version-file: scripts/generate_pinval/.python-version + python-version-file: scripts/generate_homeval/.python-version - name: Install Python requirements run: uv pip install . - working-directory: scripts/generate_pinval + working-directory: scripts/generate_homeval - name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@v4 @@ -241,7 +241,7 @@ jobs: # Unquoted neighborhood flag is intentional in this call, since we # want this to expand to nothing (rather than an empty string) if # the variable is empty - python3 scripts/generate_pinval/generate_pinval.py \ + python3 scripts/generate_homeval/generate_homeval.py \ --run-id "$RUN_ID" \ --township "$TOWNSHIP" \ --pin "${PINS[@]}" \ @@ -272,7 +272,7 @@ jobs: notify: needs: - setup - - generate-pinval + - generate-homeval runs-on: ubuntu-latest permissions: id-token: write @@ -330,9 +330,9 @@ jobs: BUCKET: ${{ needs.setup.outputs.bucket }} run: | read -ra PINS <<< "${{ inputs.pins }}" - SUBJECT="PINVAL reports ready for run-id $RUN_ID" + SUBJECT="HomeVal reports ready for run-id $RUN_ID" MESSAGE=$(cat < Date: Tue, 30 Sep 2025 14:02:52 -0500 Subject: [PATCH 4/6] Remove deprecated Quarto project --- .Renviron | 3 - .Rprofile | 1 - pinval.Rproj | 17 - pinval.qmd | 1020 ------------------------------------ renv.lock | 1211 ------------------------------------------- renv/.gitignore | 7 - renv/activate.R | 1220 -------------------------------------------- renv/settings.json | 19 - 8 files changed, 3498 deletions(-) delete mode 100644 .Renviron delete mode 100644 .Rprofile delete mode 100644 pinval.Rproj delete mode 100644 pinval.qmd delete mode 100644 renv.lock delete mode 100644 renv/.gitignore delete mode 100644 renv/activate.R delete mode 100644 renv/settings.json diff --git a/.Renviron b/.Renviron deleted file mode 100644 index a53a43cd..00000000 --- a/.Renviron +++ /dev/null @@ -1,3 +0,0 @@ -ARROW_S3=ON -ARROW_WITH_SNAPPY=ON -AWS_REGION=us-east-1 diff --git a/.Rprofile b/.Rprofile deleted file mode 100644 index 81b960f5..00000000 --- a/.Rprofile +++ /dev/null @@ -1 +0,0 @@ -source("renv/activate.R") diff --git a/pinval.Rproj b/pinval.Rproj deleted file mode 100644 index 6fc5579b..00000000 --- a/pinval.Rproj +++ /dev/null @@ -1,17 +0,0 @@ -Version: 1.0 - -RestoreWorkspace: Default -SaveWorkspace: Default -AlwaysSaveHistory: Default - -EnableCodeIndexing: Yes -UseSpacesForTab: Yes -NumSpacesForTab: 2 -Encoding: UTF-8 - -RnwWeave: Sweave -LaTeX: pdfLaTeX - -AutoAppendNewline: Yes -StripTrailingWhitespace: Yes -LineEndingConversion: Posix diff --git a/pinval.qmd b/pinval.qmd deleted file mode 100644 index f0834a36..00000000 --- a/pinval.qmd +++ /dev/null @@ -1,1020 +0,0 @@ ---- -title: "Cook County Assessor’s Model Value Report (Experimental)" -title-block-banner: "#294298" -subtitle: "PIN: `r glue::glue('{ccao::pin_format_pretty(params$pin)}')`" -execute: - echo: false - warning: false -format: - html: - embed-resources: true - fig-align: center - fontsize: 12pt - grid: - # Add a little more width to the body so the char table is full width - body-width: 1000px -knitr: - opts_chunk: - out.width: "100%" -editor: source -params: - year: "2025" - # Good 2024 PINs for testing: - # * Single card: 14051280390000 - # * Multicard: 16251020210000 - # * Past sale is a comp: 14331000240000 - # - # Good 2025 PINs for testing: - # * Single card: 05274160070000 - # * Small multicard: 05344260050000 - # * Large multicard: 05343210140000 - # * Past sale is a comp: 05333030300000 - pin: "05274160070000" - num_comps: 5 ---- - -```{r} -# This block loads data for the selected parcel, year, and model run. -# Start by loading R dependencies -library(arrow) -library(ccao) -library(dplyr) -library(noctua) -library(stringr) -library(tibble) -library(tidyr) -library(tools) - -# Helper function to wrap a PIN in a link for display purposes -link_pin <- function(pin, display_text) { - text <- ifelse(!missing(display_text), display_text, ccao::pin_format_pretty(pin)) - return( - glue::glue( - "{text}" - ) - ) -} - -# Parse run IDs for comps based on year -stopifnot( - "year param must be one of 2024 or 2025" = params$year %in% c("2024", "2025") -) -comps_run_id <- case_when( - params$year == "2024" ~ "2024-06-18-calm-nathan", - params$year == "2025" ~ "2025-04-25-fancy-free-billy" -) - -# Check if this PIN/year combo is cached, and skip data loading if so -base_cache_file_path <- glue::glue("cache/{comps_run_id}_{params$pin}_{params$year}") -cache_files <- list( - assessment = glue::glue("{base_cache_file_path}_asmt.parquet"), - comp = glue::glue("{base_cache_file_path}_comp.parquet"), - metadata = glue::glue("{base_cache_file_path}_meta.parquet") -) - -if (cache_files %>% sapply(file.exists) %>% all()) { - assessment_df <- read_parquet(cache_files$assessment) - comp_df <- read_parquet(cache_files$comp) - metadata_df <- read_parquet(cache_files$metadata) -} else { - # If any of the data are not cached, load them from S3 and Athena. - # Start by establishing an Athena connection with unload = TRUE so that - # noctua parses arrays correctly - noctua_options(unload = TRUE) - athena_conn <- noctua::athena() %>% - dbConnect( - s3_staging_dir = "s3://ccao-athena-results-us-east-1", - # Disable the Connections tab entry for this database, since we don't - # need it for the purposes of this script and it causes connection setup - # to take a long time by issuing a query for each table's schema - rstudio_conn_tab = FALSE, - ) - - # Define base paths to S3 buckets where data are stored - base_model_results_url <- "s3://ccao-model-results-us-east-1" - base_dvc_url <- "s3://ccao-data-dvc-us-east-1" - - # Get the assessment year and predictor list for characteristic queries. - # Assessment year is important since chars are defined for the year preceding - # the lien date - model_metadata_df <- dbGetQuery( - conn = athena_conn, - # This query should only ever return one row, but limit the results to 1 - # just to be defensive - glue::glue( - "SELECT assessment_data_year, model_predictor_all_name - FROM model.metadata - WHERE run_id = '{comps_run_id}' - LIMIT 1" - ) - ) - assessment_data_year <- model_metadata_df %>% pull(assessment_data_year) - all_predictors_raw <- model_metadata_df %>% - pull(model_predictor_all_name) %>% - unlist() - # Replace school district GEOIDs with names - all_predictors <- all_predictors_raw %>% - replace( - which( - all_predictors_raw %in% c( - "loc_school_elementary_district_geoid", - "loc_school_secondary_district_geoid" - ) - ), - c( - "loc_school_elementary_district_name", - "loc_school_secondary_district_name" - ) - ) - - # Load final model run for the given year, which might be different - # from the run ID specified in the params object if the comps run was - # not the same as the final model run - final_model_run_id <- dbGetQuery( - conn = athena_conn, - # As above, the LIMIT here is defensive - glue::glue(" - SELECT run_id - FROM model.final_model - WHERE year = '{params$year}' - AND type = 'res' - AND is_final - LIMIT 1 - ") - ) %>% - pull(run_id) - - final_model_run_date <- final_model_run_id %>% - substr(1, 10) %>% - as.Date() - - # Load chars that are stored in the PIN-level assessment data - assessment_pin_df <- open_dataset( - glue::glue( - "{base_model_results_url}/assessment_pin/year={params$year}/run_id={final_model_run_id}" - ) - ) %>% - filter( - meta_pin == params$pin, - meta_year == assessment_data_year - ) %>% - collect() - - subject_property_address <- assessment_pin_df %>% - # str_to_title is nice for addresses because it preserves the capitalization - # of directionals like 'N' or 'W' - mutate(property_address = loc_property_address %>% tolower() %>% str_to_title()) %>% - pull(property_address) - - pred_pin_final_fmv_round <- assessment_pin_df %>% - pull(pred_pin_final_fmv_round) - - # Save model metadata to a dedicated list for ease of access. - # Prefer a list to a dataframe since not all of the attribute values - # have the same dimensions - metadata_df <- tibble( - final_model_run_date = final_model_run_date, - subject_property_address = subject_property_address, - pred_pin_final_fmv_round = pred_pin_final_fmv_round, - all_predictors = list(all_predictors) - ) - - # Load card-level assessment data to use as the base for joining all chars - assessment_df <- open_dataset( - glue::glue( - "{base_model_results_url}/assessment_card/year={params$year}/run_id={final_model_run_id}" - ) - ) %>% - filter( - meta_pin == params$pin, - meta_year == assessment_data_year - ) %>% - collect() %>% - mutate( - township = ccao::town_convert(meta_township_code), - # toTitleCase is nice here because it preserves lowercase for prepositions - # like 'of' which are very common in municipality names - municipality = loc_tax_municipality_name %>% tolower() %>% tools::toTitleCase(), - bldg_sf = scales::comma(char_bldg_sf), - land_sf = scales::comma(char_land_sf), - pretty_pin = ccao::pin_format_pretty(meta_pin), - # Attach PIN-level chars to all cards - property_address = subject_property_address, - ) - - # Load comp data - raw_comp_df <- dbGetQuery( - conn = athena_conn, - glue::glue(" - SELECT * - FROM model.comp - WHERE pin = '{params$pin}' - AND year = '{params$year}' - AND run_id = '{comps_run_id}' - ") - ) %>% - select(-year, -run_id) %>% - as_tibble() - - # Pivot out the comp data and drop extraneous comps - pivoted_comp_df <- raw_comp_df %>% - # This requires multiple pivots joined by bind_cols because the comp attribute - # columns have different types, so they can't be pivoted in one operation. - # Start by pivoting on comp_pin - pivot_longer( - starts_with("comp_pin_"), - values_to = "comp_pin", - names_to = "comp_num", - names_pattern = "comp_pin_(\\d+)" - ) %>% - select(-starts_with("comp_score_"), -starts_with("comp_document_num_")) %>% - bind_cols( - # Next, pivot on comp_score and bind the resulting column to the dataframe - raw_comp_df %>% - pivot_longer(starts_with("comp_score_"), values_to = "comp_score") %>% - select(comp_score), - # Finally, pivot on comp_document_num and bind the column - raw_comp_df %>% - pivot_longer( - starts_with("comp_document_num_"), - values_to = "comp_document_num" - ) %>% - select(comp_document_num) - ) %>% - # Filter for only the top N comps - mutate(comp_num = as.integer(comp_num)) %>% - filter(comp_num <= params$num_comps) - - # Load training data from the DVC cache to grab characteristics for comps - dvc_md5_hash <- read_parquet( - glue::glue( - "{base_model_results_url}/metadata/year={params$year}/{comps_run_id}.parquet" - ) - ) %>% - pull(dvc_md5_training_data) - - training_data_prefix <- dvc_md5_hash %>% substr(1, 2) - training_data_filename <- dvc_md5_hash %>% substr(3, nchar(dvc_md5_hash)) - - training_df <- read_parquet( - glue::glue( - "{base_dvc_url}/files/md5/{training_data_prefix}/{training_data_filename}" - ) - ) %>% - filter( - meta_pin %in% pivoted_comp_df$comp_pin, - meta_sale_document_num %in% pivoted_comp_df$comp_document_num - ) %>% - mutate(property_address = loc_property_address %>% tolower() %>% str_to_title()) - - # Load school district data, for use in translating geo IDs to names. - # We need to do this after we've loaded assessment and training sets, since - # we only want to grab school info for the subject property and comps - school_df <- dbGetQuery( - conn = athena_conn, - glue::glue(" - SELECT - pin10, - school_elementary_district_name AS loc_school_elementary_district_name, - school_secondary_district_name AS loc_school_secondary_district_name - FROM location.school - WHERE year = '{assessment_data_year}' - AND pin10 IN ( - '{substr(params$pin, 1, 10)}', - '{pivoted_comp_df$comp_pin %>% substr(1, 10) %>% paste(collapse = \"\', \'\")}' - ) - ") - ) - - # Join school district data to assessment and training sets - assessment_df <- assessment_df %>% - mutate(meta_pin10 = substr(meta_pin, 1, 10)) %>% - left_join(school_df, by = c("meta_pin10" = "pin10")) - - training_df <- training_df %>% - left_join(school_df, by = c("meta_pin10" = "pin10")) - - # Attach training chars to comps - comp_df_with_training_chars <- pivoted_comp_df %>% - left_join( - training_df, - by = c( - "comp_pin" = "meta_pin", - "comp_document_num" = "meta_sale_document_num" - ) - ) - - # Recode all chars except char_apts since we do not code it in the training - # data in the same way we code other chars - chars_to_recode <- comp_df_with_training_chars %>% - select(starts_with("char_") & !char_apts) %>% - names() - - comp_df <- comp_df_with_training_chars %>% - # Translate coded vars to human readable values - ccao::vars_recode( - cols = chars_to_recode, - type = "long", - as_factor = FALSE - ) %>% - mutate( - bldg_sf = scales::comma(char_bldg_sf), - land_sf = scales::comma(char_land_sf), - sale_price = scales::dollar(meta_sale_price), - sale_price_short = scales::dollar( - meta_sale_price, - accuracy = 1, - scale = 1 / 1000, - suffix = "K" - ), - sale_price_per_sq_ft = (meta_sale_price / char_bldg_sf) %>% - scales::dollar(accuracy = 1), - sale_date = meta_sale_date %>% as.Date() %>% format("%h %Y"), - pretty_pin = ccao::pin_format_pretty(comp_pin), - is_subject_pin_sale = comp_pin == params$pin - ) - - # Write the assessment and comp data to the cache - write_parquet(assessment_df, cache_files$assessment) - write_parquet(comp_df, cache_files$comp) - write_parquet(metadata_df, cache_files$metadata) -} -``` - -The goal of this experimental report is to explain how the Cook County Assessor's statistical model estimated a subject home’s value. The `r format(metadata_df$final_model_run_date, "%Y")` model ran on **`r format(metadata_df$final_model_run_date, "%B %d, %Y")`**, and it used characteristics and sales available at that time to learn about the real estate market and estimate what the subject home would sell for if it sold on **January 1, `r format(metadata_df$final_model_run_date, "%Y")`**. - -::: {.callout-tip collapse="true"} -## Click to read more about this report - -This report attempts to explain the Assessor's model by explaining important inputs to the model: first, the subject home’s characteristics, and second, the top 5 experimental “sale comps.” These sale comps were identified by an experimental algorithm we built to answer the question “what sales did the model use to estimate my home’s value?” The end of the report shows the model’s estimated value for this property, which is based on the characteristics and sales input to the model. - -The sales that the model used for estimation are reported by the Illinois Department of Revenue, which records sales using the [Real Estate Transfer Declaration form](https://tax.illinois.gov/content/dam/soi/en/web/tax/localgovernments/property/documents/ptax-203.pdf). - -For more details on how the experimental sales comp algorithm works, see [Finding comparables with LightGBM](https://ccao-data.github.io/lightsnip/articles/finding-comps.html). -::: - -```{r} -library(htmltools) - -# Show a message explaining how multicards work if this PIN has multiple cards. -# This message is slightly different depending on whether this PIN is subject -# to our new small multicard handling or not -parcel_num_cards <- nrow(assessment_df) -parcel_is_post_2025_small_multicard <- ( - parcel_num_cards %in% c(2, 3) && params$year >= "2025" -) -parcel_is_pre_2025_or_large_multicard <- ( - (parcel_num_cards > 1 && params$year <= "2024") || parcel_num_cards > 3 -) -multicard_msg <- "This property has multiple \"cards\", which is an assessment term for a building or an improvement on a property." -if (parcel_is_post_2025_small_multicard) { - tags$p( - glue::glue( - "{multicard_msg} Since this property has {parcel_num_cards} cards, we estimate its value using a slightly different method than other properties. We use the characteristics of the largest card for estimation, but we adjust the building square footage of that card to reflect the combined building square footage of all cards on the property. The characteristics below reflect this difference." - ) - ) -} else if (parcel_is_pre_2025_or_large_multicard) { - tags$p( - glue::glue( - "{multicard_msg} Each card on a property can have different characteristics, so the Assessor's model estimates different values for each card. Toggle between the tabs below to view characteristics and comparable sales for each card." - ) - ) -} -``` - -```{r output="asis"} -# This block takes the data generated by the previous block and displays them -# in the report. Since multicard PINs require rendering the same elements for -# different cards in a loop, we take the approach of generating HTML -# programmatically and rendering it using "asis" output and htmltools rather -# than writing Markdown directly. This is less typical for Quarto docs but -# helpful for producing nice output for multicard parcels. -library(knitr) -library(leaflet) -library(reactable) - -# The main entrypoint function for rendering HTML elements from the data. -# Ties together a number of helper functions for returning specific data-driven -# elements like characteristic tables and a comps map. -# -# @param assessment_df Dataframe representing the chars for the subject parcel -# @param comp_df Dataframe representing chars for the comps -# @param parcel_is_multicard Boolean indicating a multicard property -# @param bldg_sf_is_combined Boolean indicating combined building SF for small multicards -# @return Returns a list of htmltools elements that can be rendered using `tagList()` -html_tags_for_report <- function(assessment_df, comp_df, parcel_is_multicard, bldg_sf_is_combined) { - return( - withTags( - list( - h2("Location"), - location_char_table(assessment_df), - h2("Characteristics"), - property_char_table(assessment_df, bldg_sf_is_combined), - h2("Top 5 comparable sales"), - comps_map(assessment_df, comp_df, parcel_is_multicard, bldg_sf_is_combined), - br(), - combined_char_table(assessment_df, comp_df, parcel_is_multicard, bldg_sf_is_combined), - subject_pin_sale_warning(comp_df, parcel_is_multicard), - h2("Summary of comparable sales"), - avg_price_summary(comp_df), - h2("Initial model estimate"), - pred_fmv_summary(assessment_df, parcel_is_multicard) - ) - ) - ) -} - -# Helper function for generating a table with location characteristics for -# the subject property -location_char_table <- function(df) { - return( - df %>% - select( - Address = property_address, - Municipality = municipality, - "Assessor Township" = township, - "Assessor Neighborhood" = meta_nbhd_code, - "Elementary School District" = loc_school_elementary_district_name, - "High School District" = loc_school_secondary_district_name - ) %>% - reactable( - sortable = FALSE - ) - ) -} - -# Helper function for generating a table with property characteristics for -# the subject property -property_char_table <- function(df, bldg_sf_is_combined) { - bldg_sf_var_name <- ifelse(bldg_sf_is_combined, "Combined Bldg. S.F.", "Bldg. S.F.") - return( - df %>% - select( - "Class" = char_class, - "Year Built" = char_yrblt, - !!bldg_sf_var_name := bldg_sf, - "Land S.F." = land_sf, - Beds = char_beds, - "Full Baths" = char_fbath, - "Half Baths" = char_hbath - ) %>% - reactable( - # Force left alignment, since otherwise numbers will be right aligned. - # We want consistent alignment since this table always only has - # one row - defaultColDef = colDef(align = "left"), - sortable = FALSE - ) - ) -} - -# Helper function for generating a leaflet map of comparable sales -comps_map <- function(assessment_df, comp_df, parcel_is_multicard, bldg_sf_is_combined) { - # Set the label of the subject property differently if it's multicard - property_label <- glue::glue("Subject {ifelse(parcel_is_multicard, 'card', 'property')}") - # Define legend attributes for the comps - legend_labels <- c( - property_label, - "Comparable sale" - ) - comp_color <- "#00cc00" - circle_marker_styles <- "width: 13px; height: 13px; border-radius: 50%" - legend_colors <- c( - glue::glue("white; {circle_marker_styles}; border: 2px solid black"), - glue::glue("{comp_color}; {circle_marker_styles}") - ) - - # Set the name of the building SF var differently when it's combined - bldg_sf_var_name <- ifelse(bldg_sf_is_combined, "Combined Bldg. S.F.", "Bldg. S.F.") - - # Set a small buffer around the map bounds so that no parcel is clipped - # when computing the bounds for the map based on parcel locations - bounds_buffer <- 0.001 - - # Generate the comps map - leaflet() %>% - addProviderTiles(providers$CartoDB.Positron) %>% - # Start by adding a circle marker for the subject property with a label - addCircleMarkers( - data = assessment_df, - ~loc_longitude, - ~loc_latitude, - opacity = 1, - fill = FALSE, - fillOpacity = 0, - weight = 2, - radius = 5, - color = "black", - # Don't display the label if the PIN is one of its own comps, - # since we will use the comp to display the label in order to - # show the most recent sale price/date. - # Use a bare `if` statement because `ifelse` can't produce NULLs - label = if (any(comp_df$is_subject_pin_sale)) { - NULL - } else { - property_label - }, - labelOptions = if (any(comp_df$is_subject_pin_sale)) { - NULL - } else { - labelOptions( - noHide = TRUE, - textsize = "12px", - direction = "top", - offset = c(0, -4) - ) - }, - # Don't display a popup if the PIN is one of its own comps, - # since we will use the comp to display the popup in that case - popup = ~ if (any(comp_df$is_subject_pin_sale)) { - NULL - } else { - paste0( - "
", property_label, "
", - "Address: ", property_address, - "
PIN: ", link_pin(meta_pin), - "
Property class: ", char_class, - "
Assessor neighborhood: ", meta_nbhd_code, - "
Year built: ", char_yrblt, - "
", bldg_sf_var_name, ": ", bldg_sf, - "
Land S.F.: ", land_sf, - "
Beds: ", char_beds, - "
Full baths: ", char_fbath, - "
Half baths: ", char_hbath - ) - } - ) %>% - # Add circle markers for the comps - addCircleMarkers( - data = comp_df, - ~loc_longitude, - ~loc_latitude, - opacity = ~ ifelse( - # Hide the marker if the comp has the same PIN as its target, since - # we want to retain the marker styles of the target but override - # the label - is_subject_pin_sale, - 0, - 1 - ), - fillOpacity = ~ ifelse( - is_subject_pin_sale, - 0, - 1 - ), - radius = 4, - color = comp_color, - label = ~ ifelse( - is_subject_pin_sale, - # It would be nice to introduce a line break here, but it doesn't - # seem like the R leaflet package accepts HTML or exposes an interface - # for advanced label formatting - glue::glue("{property_label} [{sale_price_short} ({sale_date})]"), - glue::glue("{sale_price_short} ({sale_date})") - ), - labelOptions = labelOptions( - noHide = TRUE, - textsize = "12px", - direction = "top", - opacity = 0.9, - offset = c(0, -4) - ), - popup = ~ paste0( - "
", - ifelse( - is_subject_pin_sale, - property_label, - paste0("Comp ", comp_num) - ), - "
", - "Address: ", property_address, - "
PIN: ", link_pin(comp_pin), - "
Property class: ", char_class, - "
Assessor neighborhood: ", meta_nbhd_code, - "
Sale price: ", sale_price, - "
Sale $/sqft: ", sale_price_per_sq_ft, - "
Sale date: ", sale_date, - "
Sale doc. num.: ", comp_document_num, - "
Year built: ", char_yrblt, - "
", - ifelse(is_subject_pin_sale, bldg_sf_var_name, "Bldg. S.F."), - ": ", bldg_sf, - "
Land S.F.: ", land_sf, - "
Beds: ", char_beds, - "
Full baths: ", char_fbath, - "
Half baths: ", char_hbath - ) - ) %>% - addLegend( - "bottomright", - labels = legend_labels, - colors = legend_colors, - opacity = 1 - ) %>% - addControl( - "topright", - html = "Tap or click on a dot for more details" - ) %>% - fitBounds( - # Compute the bounds for the map based on the extent of the markers - lng1 = min(assessment_df$loc_longitude, comp_df$loc_longitude) - bounds_buffer, - lat1 = min(assessment_df$loc_latitude, comp_df$loc_latitude) - bounds_buffer, - lng2 = max(assessment_df$loc_longitude, comp_df$loc_longitude) + bounds_buffer, - lat2 = max(assessment_df$loc_latitude, comp_df$loc_latitude) + bounds_buffer - ) -} - -# Helper function for generating a string summary of the average price of -# a dataframe of comps -avg_price_summary <- function(df) { - # Compute the average price and the average price per square foot - avg_sale_price <- df$meta_sale_price %>% - mean() %>% - scales::dollar() - avg_price_per_sqft <- (df$meta_sale_price / df$char_bldg_sf) %>% - mean() %>% - scales::dollar(accuracy = 1) - - # Compute the range of dates for the sales, and get a rich text HTML string - # describing the range - sale_years <- comp_df %>% - mutate(sale_year = substr(meta_sale_date, 1, 4)) %>% - arrange(sale_year) %>% - pull(sale_year) - sale_year_range_html <- ifelse( - length(unique(sale_years)) > 1, - glue::glue("between {sale_years[1]} and {sale_years[length(sale_years)]}"), - glue::glue("in {sale_years[1]}") - ) - - return( - HTML( - paste0( - "The top 5 comparable sales took place ", sale_year_range_html, ". ", - "The average price of these sales was ", - "", avg_sale_price, "", - " at ", - "", avg_price_per_sqft, "/sq.ft.", - "

" - ) - ) - ) -} - -# Helper function for generating a string summary of the predicted value of -# the subject property -pred_fmv_summary <- function(df, parcel_is_multicard) { - # Use the initial FMV since we don't care about distinguishing building - # from land value in this context - pred_fmv <- df$pred_card_initial_fmv - pred_fmv_per_sqft <- (pred_fmv / df$char_bldg_sf) - - return( - HTML( - paste0( - "

", - "Based on these and other sales, the model that ran on ", - format(metadata_df$final_model_run_date, "%B %d, %Y"), - " initially estimated that the value of this ", - ifelse(parcel_is_multicard, "card", "property"), - " as of lien date ", - "January 1, ", format(metadata_df$final_model_run_date, "%Y"), "", - " should be ", - "", pred_fmv %>% scales::dollar(), "", - " at ", - "", pred_fmv_per_sqft %>% scales::dollar(accuracy = 1), "/sq.ft.", - "

" - ) - ) - ) -} - -# Helper function to "freeze" rows in a paginated df with `rows_per_page` -# rows per page by taking `n` rows from the first page and -# copy/inserting them back into the df so they always appear as the first -# rows in the table. Useful hack for table libraries that don't allow you -# to freeze rows. -freeze_first_n_rows <- function(df, n = 2, rows_per_page = 10) { - # Grab the rows that should be frozen - frozen_rows <- df[1:n, ] - - # Initialize an empty dataframe to use for output - new_df <- data.frame() - - # Calculate the sequence of indices that we will use to insert frozen rows - df_len <- nrow(df) - insert_indices <- seq( - # Skip the first page of results - rows_per_page + 1, - df_len, - # The step length for the sequence needs to account for the frozen rows that - # we will insert - rows_per_page - n - ) - - # Construct the output dataframe by iterating the rows in the input dataframe - # and inserting the frozen rows where necessary - for (i in 1:df_len) { - if (i %in% insert_indices) { - new_df <- bind_rows(new_df, frozen_rows) - } - new_df <- bind_rows(new_df, df[i, ]) - } - - return(new_df) -} - -# Helper function for generating a table for comparing chars between the subject -# property and its comps -combined_char_table <- function(assessment_df, comp_df, parcel_is_multicard, bldg_sf_is_combined) { - # Define a list of the most important characteristics to be displayed - # first in the table - top_chars <- c( - "char_class", "meta_nbhd_code", "char_yrblt", "char_bldg_sf", - "char_land_sf", "char_beds", "char_fbath", "char_hbath" - ) - # Freeze this many of the top rows in the table - num_frozen_rows <- 3 - rows_per_page <- 10 - - # Set the name of the building SF var differently when it's combined - bldg_sf_var_name <- ifelse(bldg_sf_is_combined, "Combined Building S.F.", "Building Square Feet") - - return( - assessment_df %>% - mutate( - comp_num = "0", - meta_sale_document_num = "", - meta_sale_date = "", - meta_sale_price = "", - meta_sale_price_per_sq_ft = "", - ) %>% - select( - # If you change any chars in this list, make sure to double-check - # the top_chars vector and add or remove chars as necessary so that - # we don't accidentally add them via all_predictors - comp_num, - "Sale Price" = meta_sale_price, - "Sale $/sqft" = meta_sale_price_per_sq_ft, - "Sale Date" = meta_sale_date, - "Sale Doc. Num." = meta_sale_document_num, - "Address" = property_address, - "PIN" = pretty_pin, - "Assessor Neighborhood" = meta_nbhd_code, - all_of(top_chars), - all_of(unlist(metadata_df$all_predictors) %>% setdiff(top_chars)) - ) %>% - bind_rows( - comp_df %>% - # Add a marker to the comp number if it is a prior sale of the subject - # card - mutate( - comp_num = as.character(comp_num), - comp_num = ifelse( - is_subject_pin_sale, - paste0(comp_num, "*"), - comp_num - ) - ) %>% - select( - comp_num, - "Sale Price" = sale_price, - "Sale $/sqft" = sale_price_per_sq_ft, - "Sale Date" = sale_date, - "Sale Doc. Num." = comp_document_num, - "Address" = property_address, - "PIN" = pretty_pin, - "Assessor Neighborhood" = meta_nbhd_code, - all_of(top_chars), - all_of(unlist(metadata_df$all_predictors) %>% setdiff(top_chars)) - ) - ) %>% - # Truncate numeric values in the data to two decimal places - mutate(across(where(is.numeric), ~ round(., 2))) %>% - # Format numeric columns with commas but no decimals - mutate( - across( - c( - "char_bldg_sf", "char_land_sf", "time_sale_day", - # Exclude number of foreclosures, which needs a decimal - starts_with("prox_num") & !matches("prox_num_foreclosure_per_1000_pin_past_5_years"), - ends_with("dist_ft") - ), - scales::label_comma(accuracy = 1) - ), - ) %>% - # Format numeric columns with commas _and_ decimals - mutate(prox_num_foreclosure_per_1000_pin_past_5_years = scales::comma(prox_num_foreclosure_per_1000_pin_past_5_years, accuracy = 0.01)) %>% - # Format currency columns - mutate( - across( - c( - "acs5_median_household_renter_occupied_gross_rent", - starts_with("acs5_median_income") - ), - scales::label_dollar() - ) - ) %>% - # Format percent columns - mutate(across(starts_with("acs5_percent"), scales::label_percent())) %>% - # This percent column is already multiplied by 100, it just needs a - # percent sign - mutate(other_tax_bill_rate = paste0(other_tax_bill_rate, "%")) %>% - # Rename characteristic columns to their pretty format - ccao::vars_rename(names_from = "model", names_to = "pretty") %>% - rename("Number of Days Since First Recorded Sale" = "Sale Day") %>% - rename(!!bldg_sf_var_name := "Building Square Feet") %>% - # Pivot the dataframe longer so that we can use pagination to navigate - # through multiple pages of full characteristic values. - # Start by casting all columns to char ahead of pivot, since otherwise we - # can't combine char values into one column - mutate(across(everything(), as.character)) %>% - pivot_longer( - cols = -comp_num, - names_to = "Characteristic", - values_to = "Value" - ) %>% - # Pivot back to wide so that each property has its own column representing - # all of its chars - pivot_wider( - names_from = comp_num, - values_from = Value, - names_prefix = "Comp " - ) %>% - rename_with( - ~ ifelse(parcel_is_multicard, "Subject card", "Subject prop."), - matches("Comp 0") - ) %>% - # Freeze the price char rows so that they are always visible in the table - # even when viewing different pages - freeze_first_n_rows(n = num_frozen_rows, rows_per_page = rows_per_page) %>% - # Render a paginated table with results - reactable( - pagination = TRUE, - defaultPageSize = rows_per_page, - # Turn off sorting to retain the "frozen" price char rows - sortable = FALSE, - searchable = TRUE, - columns = list( - # Chars column should match the weight of the header row, to - # emphasize that it's acting like a header - Characteristic = colDef(style = list(fontWeight = 600)) - ), - rowStyle = function(index) { - # Style the frozen rows with a grey background and a harder bottom - # border to visually indicate that they are frozen - styles <- list() - if (index %% 10 %in% 1:num_frozen_rows) { - styles <- c(styles, background = "rgba(0, 0, 0, 0.05)") - } - if (index %% 10 == num_frozen_rows) { - styles <- c(styles, "border-bottom" = "1px solid rgb(182, 182, 182)") - } - return(styles) - }, - class = "cell-border stripe", - highlight = TRUE, - bordered = TRUE, - defaultColDef = colDef(align = "center") - ) - ) -} - -subject_pin_sale_warning <- function(comp_df, parcel_is_multicard) { - if (any(comp_df$is_subject_pin_sale)) { - return( - HTML( - paste0( - "* This comparable is a sale of the subject ", - ifelse(parcel_is_multicard, "card", "property"), ". ", - "The model typically weights these sales highly, even if they ", - "are older than other comparable sales." - ) - ) - ) - } -} - -if (parcel_is_pre_2025_or_large_multicard) { - # In case of a large (>3) multicard PIN, or a multi-card PIN valued before we - # changed our small (2-3) multicard handling in 2025, split the report out - # into tabsets, with one tabset for each card. We have to do this in raw - # Bootstrap because Quarto tabsets aren't supported by htmltools::tagList, - # and we need tagList in order to render the Leaflet map, which can't - # otherwise be rendered in the context of a `for` loop. - - # This list will store the nav elements that control the tab contents - nav_tabs <- list() - # This list will store the tab contents - tab_contents <- list() - - # Order cards so that they appear in ascending order in the report - card_nums <- assessment_df %>% - arrange(as.integer(meta_card_num)) %>% - pull(meta_card_num) - - for (card_num in card_nums) { - # Filter the df for just the row representing the card at hand - assessment_card_df <- assessment_df %>% filter(meta_card_num == card_num) - # Filter for only this card's comps - comp_card_df <- comp_df %>% filter(card == card_num) - - nav_tabs <- c( - nav_tabs, - withTags( - list( - li( - class = "nav-item", - role = "presentation", - a( - # Activate the first card's tab pane by default - class = ifelse(card_num == 1, "nav-link active", "nav-link"), - id = glue::glue("tabset-1-{card_num}-tab"), - "data-bs-toggle" = "tab", - "data-bs-target" = glue::glue("#tabset-1-{card_num}"), - role = "tab", - "aria-controls" = glue::glue("tabset-1-{card_num}"), - "aria-selected" = ifelse(card_num == 1, "true", "false"), - href = "", - paste0("Card ", card_num) - ) - ) - ) - ) - ) - - tab_contents <- c( - tab_contents, - withTags( - list( - div( - id = glue::glue("tabset-1-{card_num}"), - class = ifelse(card_num == 1, "tab-pane active", "tab-pane"), - role = "tabpanel", - "aria-labelledby" = glue::glue("tabset-1-{card_num}-tab"), - html_tags_for_report( - assessment_card_df, - comp_card_df, - parcel_is_multicard = TRUE, - bldg_sf_is_combined = FALSE - ) - ) - ) - ) - ) - } - - html_tags <- withTags( - list( - div( - class = "panel-tabset", - ul( - class = "nav nav-tabs", - role = "tablist", - nav_tabs - ), - div( - class = "tab-content", - tab_contents - ) - ) - ) - ) -} else if (parcel_is_post_2025_small_multicard) { - # Handle small (2-3) multicard PINs slightly differently if they were valued - # during or after 2025, to account for the way we changed small multicard - # valuation in that year to use the largest card in the PIN with a composite - # building square footage. - assessment_frankencard_df <- assessment_df %>% - arrange(desc(char_bldg_sf), meta_card_num) %>% - mutate(char_bldg_sf = sum(char_bldg_sf)) %>% - first() - - comp_frankencard_df <- comp_df %>% - filter(card == pull(assessment_frankencard_df, meta_card_num)) - - html_tags <- html_tags_for_report( - assessment_frankencard_df, - comp_frankencard_df, - parcel_is_multicard = FALSE, - bldg_sf_is_combined = TRUE - ) -} else { - # For single-card PINs, skip the tablist - html_tags <- html_tags_for_report( - assessment_df, - comp_df, - parcel_is_multicard = FALSE, - bldg_sf_is_combined = FALSE - ) -} - -# Render the list of HTML tags -tagList(html_tags) -``` - -## Final model estimate - -After rounding and other processing, the model's final estimate for the value of this property on lien date **January 1st, `r format(metadata_df$final_model_run_date, "%Y")`** was **`r scales::dollar(metadata_df$pred_pin_final_fmv_round)`**. - -The model's estimated value for this property is not necessarily the final valuation during a reassessment. Analysts at the Assessor's Office can review the model's estimate and make adjustments. To see this property's most recent valuation, visit the `r link_pin(params$pin, "Assessor's website")`. diff --git a/renv.lock b/renv.lock deleted file mode 100644 index 4a082643..00000000 --- a/renv.lock +++ /dev/null @@ -1,1211 +0,0 @@ -{ - "R": { - "Version": "4.4.1", - "Repositories": [ - { - "Name": "CRAN", - "URL": "https://cloud.r-project.org" - } - ] - }, - "Packages": { - "DBI": { - "Package": "DBI", - "Version": "1.2.3", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "methods" - ], - "Hash": "065ae649b05f1ff66bb0c793107508f5" - }, - "R6": { - "Package": "R6", - "Version": "2.5.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "470851b6d5d0ac559e9d01bb352b4021" - }, - "RColorBrewer": { - "Package": "RColorBrewer", - "Version": "1.1-3", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "45f0398006e83a5b10b72a90663d8d8c" - }, - "Rcpp": { - "Package": "Rcpp", - "Version": "1.0.12", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "methods", - "utils" - ], - "Hash": "5ea2700d21e038ace58269ecdbeb9ec0" - }, - "arrow": { - "Package": "arrow", - "Version": "16.1.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "R6", - "assertthat", - "bit64", - "cpp11", - "glue", - "methods", - "purrr", - "rlang", - "stats", - "tidyselect", - "utils", - "vctrs" - ], - "Hash": "d9cb18a8fc53895548150650cadde3a6" - }, - "askpass": { - "Package": "askpass", - "Version": "1.2.0", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "sys" - ], - "Hash": "cad6cf7f1d5f6e906700b9d3e718c796" - }, - "assertthat": { - "Package": "assertthat", - "Version": "0.2.1", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "tools" - ], - "Hash": "50c838a310445e954bc13f26f26a6ecf" - }, - "assessr": { - "Package": "assessr", - "Version": "0.6.0", - "Source": "GitHub", - "RemoteType": "github", - "RemoteUsername": "ccao-data", - "RemoteRepo": "assessr", - "RemoteRef": "master", - "RemoteSha": "82b3cca6374cc7300145e4345278329b7835086a", - "RemoteHost": "api.github.com", - "Requirements": [ - "R", - "stats" - ], - "Hash": "8150134e9077ee9edd809ac873d268f9" - }, - "base64enc": { - "Package": "base64enc", - "Version": "0.1-3", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R" - ], - "Hash": "543776ae6848fde2f48ff3816d0628bc" - }, - "bit": { - "Package": "bit", - "Version": "4.0.5", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R" - ], - "Hash": "d242abec29412ce988848d0294b208fd" - }, - "bit64": { - "Package": "bit64", - "Version": "4.0.5", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "bit", - "methods", - "stats", - "utils" - ], - "Hash": "9fe98599ca456d6552421db0d6772d8f" - }, - "bslib": { - "Package": "bslib", - "Version": "0.7.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "base64enc", - "cachem", - "fastmap", - "grDevices", - "htmltools", - "jquerylib", - "jsonlite", - "lifecycle", - "memoise", - "mime", - "rlang", - "sass" - ], - "Hash": "8644cc53f43828f19133548195d7e59e" - }, - "cachem": { - "Package": "cachem", - "Version": "1.1.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "fastmap", - "rlang" - ], - "Hash": "cd9a672193789068eb5a2aad65a0dedf" - }, - "ccao": { - "Package": "ccao", - "Version": "1.3.0", - "Source": "GitHub", - "RemoteType": "github", - "RemoteUsername": "ccao-data", - "RemoteRepo": "ccao", - "RemoteRef": "master", - "RemoteSha": "d85e0d4729b12f1bd2bc200b0b8a93ccca5cdd38", - "RemoteHost": "api.github.com", - "Remotes": "ccao-data/assessr", - "Requirements": [ - "R", - "assessr", - "dplyr", - "magrittr", - "rlang", - "tidyr" - ], - "Hash": "77296c19d597360a5ec2fc30436322fc" - }, - "cli": { - "Package": "cli", - "Version": "3.6.3", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "utils" - ], - "Hash": "b21916dd77a27642b447374a5d30ecf3" - }, - "colorspace": { - "Package": "colorspace", - "Version": "2.1-0", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "grDevices", - "graphics", - "methods", - "stats" - ], - "Hash": "f20c47fd52fae58b4e377c37bb8c335b" - }, - "cpp11": { - "Package": "cpp11", - "Version": "0.4.7", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "5a295d7d963cc5035284dcdbaf334f4e" - }, - "crosstalk": { - "Package": "crosstalk", - "Version": "1.2.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R6", - "htmltools", - "jsonlite", - "lazyeval" - ], - "Hash": "ab12c7b080a57475248a30f4db6298c0" - }, - "curl": { - "Package": "curl", - "Version": "5.2.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "411ca2c03b1ce5f548345d2fc2685f7a" - }, - "data.table": { - "Package": "data.table", - "Version": "1.15.4", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "methods" - ], - "Hash": "8ee9ac56ef633d0c7cab8b2ca87d683e" - }, - "digest": { - "Package": "digest", - "Version": "0.6.36", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "utils" - ], - "Hash": "fd6824ad91ede64151e93af67df6376b" - }, - "dplyr": { - "Package": "dplyr", - "Version": "1.1.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "R6", - "cli", - "generics", - "glue", - "lifecycle", - "magrittr", - "methods", - "pillar", - "rlang", - "tibble", - "tidyselect", - "utils", - "vctrs" - ], - "Hash": "fedd9d00c2944ff00a0e2696ccf048ec" - }, - "evaluate": { - "Package": "evaluate", - "Version": "0.24.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "methods" - ], - "Hash": "a1066cbc05caee9a4bf6d90f194ff4da" - }, - "fansi": { - "Package": "fansi", - "Version": "1.0.6", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "grDevices", - "utils" - ], - "Hash": "962174cf2aeb5b9eea581522286a911f" - }, - "farver": { - "Package": "farver", - "Version": "2.1.2", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "680887028577f3fa2a81e410ed0d6e42" - }, - "fastmap": { - "Package": "fastmap", - "Version": "1.2.0", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "aa5e1cd11c2d15497494c5292d7ffcc8" - }, - "fontawesome": { - "Package": "fontawesome", - "Version": "0.5.2", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "htmltools", - "rlang" - ], - "Hash": "c2efdd5f0bcd1ea861c2d4e2a883a67d" - }, - "fs": { - "Package": "fs", - "Version": "1.6.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "methods" - ], - "Hash": "15aeb8c27f5ea5161f9f6a641fafd93a" - }, - "generics": { - "Package": "generics", - "Version": "0.1.3", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "methods" - ], - "Hash": "15e9634c0fcd294799e9b2e929ed1b86" - }, - "glue": { - "Package": "glue", - "Version": "1.7.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "methods" - ], - "Hash": "e0b3a53876554bd45879e596cdb10a52" - }, - "highr": { - "Package": "highr", - "Version": "0.11", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "xfun" - ], - "Hash": "d65ba49117ca223614f71b60d85b8ab7" - }, - "htmltools": { - "Package": "htmltools", - "Version": "0.5.8.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "base64enc", - "digest", - "fastmap", - "grDevices", - "rlang", - "utils" - ], - "Hash": "81d371a9cc60640e74e4ab6ac46dcedc" - }, - "htmlwidgets": { - "Package": "htmlwidgets", - "Version": "1.6.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "grDevices", - "htmltools", - "jsonlite", - "knitr", - "rmarkdown", - "yaml" - ], - "Hash": "04291cc45198225444a397606810ac37" - }, - "httr": { - "Package": "httr", - "Version": "1.4.7", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "R6", - "curl", - "jsonlite", - "mime", - "openssl" - ], - "Hash": "ac107251d9d9fd72f0ca8049988f1d7f" - }, - "jquerylib": { - "Package": "jquerylib", - "Version": "0.1.4", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "htmltools" - ], - "Hash": "5aab57a3bd297eee1c1d862735972182" - }, - "jsonlite": { - "Package": "jsonlite", - "Version": "1.8.8", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "methods" - ], - "Hash": "e1b9c55281c5adc4dd113652d9e26768" - }, - "knitr": { - "Package": "knitr", - "Version": "1.47", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "evaluate", - "highr", - "methods", - "tools", - "xfun", - "yaml" - ], - "Hash": "7c99b2d55584b982717fcc0950378612" - }, - "labeling": { - "Package": "labeling", - "Version": "0.4.3", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "graphics", - "stats" - ], - "Hash": "b64ec208ac5bc1852b285f665d6368b3" - }, - "lattice": { - "Package": "lattice", - "Version": "0.22-5", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "grDevices", - "graphics", - "grid", - "stats", - "utils" - ], - "Hash": "7c5e89f04e72d6611c77451f6331a091" - }, - "lazyeval": { - "Package": "lazyeval", - "Version": "0.2.2", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R" - ], - "Hash": "d908914ae53b04d4c0c0fd72ecc35370" - }, - "leaflet": { - "Package": "leaflet", - "Version": "2.2.2", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "RColorBrewer", - "crosstalk", - "htmltools", - "htmlwidgets", - "jquerylib", - "leaflet.providers", - "magrittr", - "methods", - "png", - "raster", - "scales", - "sp", - "stats", - "viridisLite", - "xfun" - ], - "Hash": "ca012d4a706e21ce217ba15f22d402b2" - }, - "leaflet.providers": { - "Package": "leaflet.providers", - "Version": "2.0.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "htmltools" - ], - "Hash": "c0b81ad9d5d932772f7a457ac398cf36" - }, - "lifecycle": { - "Package": "lifecycle", - "Version": "1.0.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "cli", - "glue", - "rlang" - ], - "Hash": "b8552d117e1b808b09a832f589b79035" - }, - "magrittr": { - "Package": "magrittr", - "Version": "2.0.3", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "7ce2733a9826b3aeb1775d56fd305472" - }, - "memoise": { - "Package": "memoise", - "Version": "2.0.1", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "cachem", - "rlang" - ], - "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" - }, - "mime": { - "Package": "mime", - "Version": "0.12", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "tools" - ], - "Hash": "18e9c28c1d3ca1560ce30658b22ce104" - }, - "munsell": { - "Package": "munsell", - "Version": "0.5.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "colorspace", - "methods" - ], - "Hash": "4fd8900853b746af55b81fda99da7695" - }, - "noctua": { - "Package": "noctua", - "Version": "2.6.2", - "Source": "GitHub", - "RemoteType": "github", - "RemoteHost": "api.github.com", - "RemoteUsername": "DyfanJones", - "RemoteRepo": "noctua", - "RemoteRef": "master", - "RemoteSha": "23a4cfbf537407c7a1547fc13ba771ba2eb098e0", - "Requirements": [ - "DBI", - "R", - "data.table", - "methods", - "paws", - "stats", - "utils", - "uuid" - ], - "Hash": "a48e1decdd027c44ea6b97b0fe0950cb" - }, - "openssl": { - "Package": "openssl", - "Version": "2.2.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "askpass" - ], - "Hash": "2bcca3848e4734eb3b16103bc9aa4b8e" - }, - "paws": { - "Package": "paws", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.analytics", - "paws.application.integration", - "paws.common", - "paws.compute", - "paws.cost.management", - "paws.customer.engagement", - "paws.database", - "paws.developer.tools", - "paws.end.user.computing", - "paws.machine.learning", - "paws.management", - "paws.networking", - "paws.security.identity", - "paws.storage" - ], - "Hash": "6260ba57b1cfca3ed9e74d56441730eb" - }, - "paws.analytics": { - "Package": "paws.analytics", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "d8ea9a4dfefe8a49eceff027e053aa2f" - }, - "paws.application.integration": { - "Package": "paws.application.integration", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "4a768547ec143a4a0742b33929010da4" - }, - "paws.common": { - "Package": "paws.common", - "Version": "0.7.3", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "Rcpp", - "base64enc", - "curl", - "digest", - "httr", - "jsonlite", - "methods", - "stats", - "utils", - "xml2" - ], - "Hash": "cac0fa401210ea975f052359afa894e2" - }, - "paws.compute": { - "Package": "paws.compute", - "Version": "0.6.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "f9a130a8f63986177bd497037b2e0447" - }, - "paws.cost.management": { - "Package": "paws.cost.management", - "Version": "0.6.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "756e7414ddce9aecc9a33650ac60c903" - }, - "paws.customer.engagement": { - "Package": "paws.customer.engagement", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "ff913af420713815ade6aec300c0390d" - }, - "paws.database": { - "Package": "paws.database", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "f46bbfaad83b675c56b19a44555a0a45" - }, - "paws.developer.tools": { - "Package": "paws.developer.tools", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "6e32fbe7df12ab9d3376865032218d55" - }, - "paws.end.user.computing": { - "Package": "paws.end.user.computing", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "e9de6ac1785b5f2520184f6aec59ac61" - }, - "paws.machine.learning": { - "Package": "paws.machine.learning", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "4d6b5c39490c366bdba2783f9c2727dd" - }, - "paws.management": { - "Package": "paws.management", - "Version": "0.6.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "3ded918fc46f96877fa3e00168de518e" - }, - "paws.networking": { - "Package": "paws.networking", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "c21930277e2d5a74937bcdf1dc0fc574" - }, - "paws.security.identity": { - "Package": "paws.security.identity", - "Version": "0.6.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "48f2f348334a7186a73bb8d58420baed" - }, - "paws.storage": { - "Package": "paws.storage", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "paws.common" - ], - "Hash": "86a93407f22e5855723b83988a142dcb" - }, - "pillar": { - "Package": "pillar", - "Version": "1.9.0", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "cli", - "fansi", - "glue", - "lifecycle", - "rlang", - "utf8", - "utils", - "vctrs" - ], - "Hash": "15da5a8412f317beeee6175fbc76f4bb" - }, - "pkgconfig": { - "Package": "pkgconfig", - "Version": "2.0.3", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "utils" - ], - "Hash": "01f28d4278f15c76cddbea05899c5d6f" - }, - "png": { - "Package": "png", - "Version": "0.1-8", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "bd54ba8a0a5faded999a7aab6e46b374" - }, - "purrr": { - "Package": "purrr", - "Version": "1.0.2", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "cli", - "lifecycle", - "magrittr", - "rlang", - "vctrs" - ], - "Hash": "1cba04a4e9414bdefc9dcaa99649a8dc" - }, - "rappdirs": { - "Package": "rappdirs", - "Version": "0.3.3", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R" - ], - "Hash": "5e3c5dc0b071b21fa128676560dbe94d" - }, - "raster": { - "Package": "raster", - "Version": "3.6-26", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "Rcpp", - "methods", - "sp", - "terra" - ], - "Hash": "7d6eda494f34a644420ac1bfd2a8023a" - }, - "reactR": { - "Package": "reactR", - "Version": "0.6.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "htmltools" - ], - "Hash": "10f4d661c235181648a5958c02c0382a" - }, - "reactable": { - "Package": "reactable", - "Version": "0.4.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "digest", - "htmltools", - "htmlwidgets", - "jsonlite", - "reactR" - ], - "Hash": "6069eb2a6597963eae0605c1875ff14c" - }, - "renv": { - "Package": "renv", - "Version": "1.0.7", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "utils" - ], - "Hash": "397b7b2a265bc5a7a06852524dabae20" - }, - "rlang": { - "Package": "rlang", - "Version": "1.1.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "utils" - ], - "Hash": "3eec01f8b1dee337674b2e34ab1f9bc1" - }, - "rmarkdown": { - "Package": "rmarkdown", - "Version": "2.27", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "bslib", - "evaluate", - "fontawesome", - "htmltools", - "jquerylib", - "jsonlite", - "knitr", - "methods", - "tinytex", - "tools", - "utils", - "xfun", - "yaml" - ], - "Hash": "27f9502e1cdbfa195f94e03b0f517484" - }, - "sass": { - "Package": "sass", - "Version": "0.4.9", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R6", - "fs", - "htmltools", - "rappdirs", - "rlang" - ], - "Hash": "d53dbfddf695303ea4ad66f86e99b95d" - }, - "scales": { - "Package": "scales", - "Version": "1.3.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "R6", - "RColorBrewer", - "cli", - "farver", - "glue", - "labeling", - "lifecycle", - "munsell", - "rlang", - "viridisLite" - ], - "Hash": "c19df082ba346b0ffa6f833e92de34d1" - }, - "sp": { - "Package": "sp", - "Version": "2.1-4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "grDevices", - "graphics", - "grid", - "lattice", - "methods", - "stats", - "utils" - ], - "Hash": "75940133cca2e339afce15a586f85b11" - }, - "stringi": { - "Package": "stringi", - "Version": "1.8.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "stats", - "tools", - "utils" - ], - "Hash": "39e1144fd75428983dc3f63aa53dfa91" - }, - "stringr": { - "Package": "stringr", - "Version": "1.5.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "cli", - "glue", - "lifecycle", - "magrittr", - "rlang", - "stringi", - "vctrs" - ], - "Hash": "960e2ae9e09656611e0b8214ad543207" - }, - "sys": { - "Package": "sys", - "Version": "3.4.2", - "Source": "Repository", - "Repository": "RSPM", - "Hash": "3a1be13d68d47a8cd0bfd74739ca1555" - }, - "terra": { - "Package": "terra", - "Version": "1.7-78", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "Rcpp", - "methods" - ], - "Hash": "8f020def0792119cb98bd8f696dab22d" - }, - "tibble": { - "Package": "tibble", - "Version": "3.2.1", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "fansi", - "lifecycle", - "magrittr", - "methods", - "pillar", - "pkgconfig", - "rlang", - "utils", - "vctrs" - ], - "Hash": "a84e2cc86d07289b3b6f5069df7a004c" - }, - "tidyr": { - "Package": "tidyr", - "Version": "1.3.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "cli", - "cpp11", - "dplyr", - "glue", - "lifecycle", - "magrittr", - "purrr", - "rlang", - "stringr", - "tibble", - "tidyselect", - "utils", - "vctrs" - ], - "Hash": "915fb7ce036c22a6a33b5a8adb712eb1" - }, - "tidyselect": { - "Package": "tidyselect", - "Version": "1.2.1", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R", - "cli", - "glue", - "lifecycle", - "rlang", - "vctrs", - "withr" - ], - "Hash": "829f27b9c4919c16b593794a6344d6c0" - }, - "tinytex": { - "Package": "tinytex", - "Version": "0.51", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "xfun" - ], - "Hash": "d44e2fcd2e4e076f0aac540208559d1d" - }, - "utf8": { - "Package": "utf8", - "Version": "1.2.4", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "62b65c52671e6665f803ff02954446e9" - }, - "uuid": { - "Package": "uuid", - "Version": "1.2-0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R" - ], - "Hash": "303c19bfd970bece872f93a824e323d9" - }, - "vctrs": { - "Package": "vctrs", - "Version": "0.6.5", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "cli", - "glue", - "lifecycle", - "rlang" - ], - "Hash": "c03fa420630029418f7e6da3667aac4a" - }, - "viridisLite": { - "Package": "viridisLite", - "Version": "0.4.2", - "Source": "Repository", - "Repository": "RSPM", - "Requirements": [ - "R" - ], - "Hash": "c826c7c4241b6fc89ff55aaea3fa7491" - }, - "withr": { - "Package": "withr", - "Version": "3.0.0", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "grDevices", - "graphics" - ], - "Hash": "d31b6c62c10dcf11ec530ca6b0dd5d35" - }, - "xfun": { - "Package": "xfun", - "Version": "0.45", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "grDevices", - "stats", - "tools" - ], - "Hash": "ca59c87fe305b16a9141a5874c3a7889" - }, - "xml2": { - "Package": "xml2", - "Version": "1.3.6", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "R", - "cli", - "methods", - "rlang" - ], - "Hash": "1d0336142f4cd25d8d23cd3ba7a8fb61" - }, - "yaml": { - "Package": "yaml", - "Version": "2.3.8", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "29240487a071f535f5e5d5a323b7afbd" - } - } -} diff --git a/renv/.gitignore b/renv/.gitignore deleted file mode 100644 index 0ec0cbba..00000000 --- a/renv/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -library/ -local/ -cellar/ -lock/ -python/ -sandbox/ -staging/ diff --git a/renv/activate.R b/renv/activate.R deleted file mode 100644 index d13f9932..00000000 --- a/renv/activate.R +++ /dev/null @@ -1,1220 +0,0 @@ - -local({ - - # the requested version of renv - version <- "1.0.7" - attr(version, "sha") <- NULL - - # the project directory - project <- Sys.getenv("RENV_PROJECT") - if (!nzchar(project)) - project <- getwd() - - # use start-up diagnostics if enabled - diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") - if (diagnostics) { - start <- Sys.time() - profile <- tempfile("renv-startup-", fileext = ".Rprof") - utils::Rprof(profile) - on.exit({ - utils::Rprof(NULL) - elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) - writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) - writeLines(sprintf("- Profile: %s", profile)) - print(utils::summaryRprof(profile)) - }, add = TRUE) - } - - # figure out whether the autoloader is enabled - enabled <- local({ - - # first, check config option - override <- getOption("renv.config.autoloader.enabled") - if (!is.null(override)) - return(override) - - # if we're being run in a context where R_LIBS is already set, - # don't load -- presumably we're being run as a sub-process and - # the parent process has already set up library paths for us - rcmd <- Sys.getenv("R_CMD", unset = NA) - rlibs <- Sys.getenv("R_LIBS", unset = NA) - if (!is.na(rlibs) && !is.na(rcmd)) - return(FALSE) - - # next, check environment variables - # TODO: prefer using the configuration one in the future - envvars <- c( - "RENV_CONFIG_AUTOLOADER_ENABLED", - "RENV_AUTOLOADER_ENABLED", - "RENV_ACTIVATE_PROJECT" - ) - - for (envvar in envvars) { - envval <- Sys.getenv(envvar, unset = NA) - if (!is.na(envval)) - return(tolower(envval) %in% c("true", "t", "1")) - } - - # enable by default - TRUE - - }) - - # bail if we're not enabled - if (!enabled) { - - # if we're not enabled, we might still need to manually load - # the user profile here - profile <- Sys.getenv("R_PROFILE_USER", unset = "~/.Rprofile") - if (file.exists(profile)) { - cfg <- Sys.getenv("RENV_CONFIG_USER_PROFILE", unset = "TRUE") - if (tolower(cfg) %in% c("true", "t", "1")) - sys.source(profile, envir = globalenv()) - } - - return(FALSE) - - } - - # avoid recursion - if (identical(getOption("renv.autoloader.running"), TRUE)) { - warning("ignoring recursive attempt to run renv autoloader") - return(invisible(TRUE)) - } - - # signal that we're loading renv during R startup - options(renv.autoloader.running = TRUE) - on.exit(options(renv.autoloader.running = NULL), add = TRUE) - - # signal that we've consented to use renv - options(renv.consent = TRUE) - - # load the 'utils' package eagerly -- this ensures that renv shims, which - # mask 'utils' packages, will come first on the search path - library(utils, lib.loc = .Library) - - # unload renv if it's already been loaded - if ("renv" %in% loadedNamespaces()) - unloadNamespace("renv") - - # load bootstrap tools - `%||%` <- function(x, y) { - if (is.null(x)) y else x - } - - catf <- function(fmt, ..., appendLF = TRUE) { - - quiet <- getOption("renv.bootstrap.quiet", default = FALSE) - if (quiet) - return(invisible()) - - msg <- sprintf(fmt, ...) - cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") - - invisible(msg) - - } - - header <- function(label, - ..., - prefix = "#", - suffix = "-", - n = min(getOption("width"), 78)) - { - label <- sprintf(label, ...) - n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) - if (n <= 0) - return(paste(prefix, label)) - - tail <- paste(rep.int(suffix, n), collapse = "") - paste0(prefix, " ", label, " ", tail) - - } - - heredoc <- function(text, leave = 0) { - - # remove leading, trailing whitespace - trimmed <- gsub("^\\s*\\n|\\n\\s*$", "", text) - - # split into lines - lines <- strsplit(trimmed, "\n", fixed = TRUE)[[1L]] - - # compute common indent - indent <- regexpr("[^[:space:]]", lines) - common <- min(setdiff(indent, -1L)) - leave - paste(substring(lines, common), collapse = "\n") - - } - - startswith <- function(string, prefix) { - substring(string, 1, nchar(prefix)) == prefix - } - - bootstrap <- function(version, library) { - - friendly <- renv_bootstrap_version_friendly(version) - section <- header(sprintf("Bootstrapping renv %s", friendly)) - catf(section) - - # attempt to download renv - catf("- Downloading renv ... ", appendLF = FALSE) - withCallingHandlers( - tarball <- renv_bootstrap_download(version), - error = function(err) { - catf("FAILED") - stop("failed to download:\n", conditionMessage(err)) - } - ) - catf("OK") - on.exit(unlink(tarball), add = TRUE) - - # now attempt to install - catf("- Installing renv ... ", appendLF = FALSE) - withCallingHandlers( - status <- renv_bootstrap_install(version, tarball, library), - error = function(err) { - catf("FAILED") - stop("failed to install:\n", conditionMessage(err)) - } - ) - catf("OK") - - # add empty line to break up bootstrapping from normal output - catf("") - - return(invisible()) - } - - renv_bootstrap_tests_running <- function() { - getOption("renv.tests.running", default = FALSE) - } - - renv_bootstrap_repos <- function() { - - # get CRAN repository - cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") - - # check for repos override - repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) - if (!is.na(repos)) { - - # check for RSPM; if set, use a fallback repository for renv - rspm <- Sys.getenv("RSPM", unset = NA) - if (identical(rspm, repos)) - repos <- c(RSPM = rspm, CRAN = cran) - - return(repos) - - } - - # check for lockfile repositories - repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) - if (!inherits(repos, "error") && length(repos)) - return(repos) - - # retrieve current repos - repos <- getOption("repos") - - # ensure @CRAN@ entries are resolved - repos[repos == "@CRAN@"] <- cran - - # add in renv.bootstrap.repos if set - default <- c(FALLBACK = "https://cloud.r-project.org") - extra <- getOption("renv.bootstrap.repos", default = default) - repos <- c(repos, extra) - - # remove duplicates that might've snuck in - dupes <- duplicated(repos) | duplicated(names(repos)) - repos[!dupes] - - } - - renv_bootstrap_repos_lockfile <- function() { - - lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") - if (!file.exists(lockpath)) - return(NULL) - - lockfile <- tryCatch(renv_json_read(lockpath), error = identity) - if (inherits(lockfile, "error")) { - warning(lockfile) - return(NULL) - } - - repos <- lockfile$R$Repositories - if (length(repos) == 0) - return(NULL) - - keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) - vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) - names(vals) <- keys - - return(vals) - - } - - renv_bootstrap_download <- function(version) { - - sha <- attr(version, "sha", exact = TRUE) - - methods <- if (!is.null(sha)) { - - # attempting to bootstrap a development version of renv - c( - function() renv_bootstrap_download_tarball(sha), - function() renv_bootstrap_download_github(sha) - ) - - } else { - - # attempting to bootstrap a release version of renv - c( - function() renv_bootstrap_download_tarball(version), - function() renv_bootstrap_download_cran_latest(version), - function() renv_bootstrap_download_cran_archive(version) - ) - - } - - for (method in methods) { - path <- tryCatch(method(), error = identity) - if (is.character(path) && file.exists(path)) - return(path) - } - - stop("All download methods failed") - - } - - renv_bootstrap_download_impl <- function(url, destfile) { - - mode <- "wb" - - # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 - fixup <- - Sys.info()[["sysname"]] == "Windows" && - substring(url, 1L, 5L) == "file:" - - if (fixup) - mode <- "w+b" - - args <- list( - url = url, - destfile = destfile, - mode = mode, - quiet = TRUE - ) - - if ("headers" %in% names(formals(utils::download.file))) - args$headers <- renv_bootstrap_download_custom_headers(url) - - do.call(utils::download.file, args) - - } - - renv_bootstrap_download_custom_headers <- function(url) { - - headers <- getOption("renv.download.headers") - if (is.null(headers)) - return(character()) - - if (!is.function(headers)) - stopf("'renv.download.headers' is not a function") - - headers <- headers(url) - if (length(headers) == 0L) - return(character()) - - if (is.list(headers)) - headers <- unlist(headers, recursive = FALSE, use.names = TRUE) - - ok <- - is.character(headers) && - is.character(names(headers)) && - all(nzchar(names(headers))) - - if (!ok) - stop("invocation of 'renv.download.headers' did not return a named character vector") - - headers - - } - - renv_bootstrap_download_cran_latest <- function(version) { - - spec <- renv_bootstrap_download_cran_latest_find(version) - type <- spec$type - repos <- spec$repos - - baseurl <- utils::contrib.url(repos = repos, type = type) - ext <- if (identical(type, "source")) - ".tar.gz" - else if (Sys.info()[["sysname"]] == "Windows") - ".zip" - else - ".tgz" - name <- sprintf("renv_%s%s", version, ext) - url <- paste(baseurl, name, sep = "/") - - destfile <- file.path(tempdir(), name) - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (inherits(status, "condition")) - return(FALSE) - - # report success and return - destfile - - } - - renv_bootstrap_download_cran_latest_find <- function(version) { - - # check whether binaries are supported on this system - binary <- - getOption("renv.bootstrap.binary", default = TRUE) && - !identical(.Platform$pkgType, "source") && - !identical(getOption("pkgType"), "source") && - Sys.info()[["sysname"]] %in% c("Darwin", "Windows") - - types <- c(if (binary) "binary", "source") - - # iterate over types + repositories - for (type in types) { - for (repos in renv_bootstrap_repos()) { - - # retrieve package database - db <- tryCatch( - as.data.frame( - utils::available.packages(type = type, repos = repos), - stringsAsFactors = FALSE - ), - error = identity - ) - - if (inherits(db, "error")) - next - - # check for compatible entry - entry <- db[db$Package %in% "renv" & db$Version %in% version, ] - if (nrow(entry) == 0) - next - - # found it; return spec to caller - spec <- list(entry = entry, type = type, repos = repos) - return(spec) - - } - } - - # if we got here, we failed to find renv - fmt <- "renv %s is not available from your declared package repositories" - stop(sprintf(fmt, version)) - - } - - renv_bootstrap_download_cran_archive <- function(version) { - - name <- sprintf("renv_%s.tar.gz", version) - repos <- renv_bootstrap_repos() - urls <- file.path(repos, "src/contrib/Archive/renv", name) - destfile <- file.path(tempdir(), name) - - for (url in urls) { - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (identical(status, 0L)) - return(destfile) - - } - - return(FALSE) - - } - - renv_bootstrap_download_tarball <- function(version) { - - # if the user has provided the path to a tarball via - # an environment variable, then use it - tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) - if (is.na(tarball)) - return() - - # allow directories - if (dir.exists(tarball)) { - name <- sprintf("renv_%s.tar.gz", version) - tarball <- file.path(tarball, name) - } - - # bail if it doesn't exist - if (!file.exists(tarball)) { - - # let the user know we weren't able to honour their request - fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." - msg <- sprintf(fmt, tarball) - warning(msg) - - # bail - return() - - } - - catf("- Using local tarball '%s'.", tarball) - tarball - - } - - renv_bootstrap_download_github <- function(version) { - - enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") - if (!identical(enabled, "TRUE")) - return(FALSE) - - # prepare download options - pat <- Sys.getenv("GITHUB_PAT") - if (nzchar(Sys.which("curl")) && nzchar(pat)) { - fmt <- "--location --fail --header \"Authorization: token %s\"" - extra <- sprintf(fmt, pat) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "curl", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { - fmt <- "--header=\"Authorization: token %s\"" - extra <- sprintf(fmt, pat) - saved <- options("download.file.method", "download.file.extra") - options(download.file.method = "wget", download.file.extra = extra) - on.exit(do.call(base::options, saved), add = TRUE) - } - - url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) - name <- sprintf("renv_%s.tar.gz", version) - destfile <- file.path(tempdir(), name) - - status <- tryCatch( - renv_bootstrap_download_impl(url, destfile), - condition = identity - ) - - if (!identical(status, 0L)) - return(FALSE) - - renv_bootstrap_download_augment(destfile) - - return(destfile) - - } - - # Add Sha to DESCRIPTION. This is stop gap until #890, after which we - # can use renv::install() to fully capture metadata. - renv_bootstrap_download_augment <- function(destfile) { - sha <- renv_bootstrap_git_extract_sha1_tar(destfile) - if (is.null(sha)) { - return() - } - - # Untar - tempdir <- tempfile("renv-github-") - on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) - untar(destfile, exdir = tempdir) - pkgdir <- dir(tempdir, full.names = TRUE)[[1]] - - # Modify description - desc_path <- file.path(pkgdir, "DESCRIPTION") - desc_lines <- readLines(desc_path) - remotes_fields <- c( - "RemoteType: github", - "RemoteHost: api.github.com", - "RemoteRepo: renv", - "RemoteUsername: rstudio", - "RemotePkgRef: rstudio/renv", - paste("RemoteRef: ", sha), - paste("RemoteSha: ", sha) - ) - writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) - - # Re-tar - local({ - old <- setwd(tempdir) - on.exit(setwd(old), add = TRUE) - - tar(destfile, compression = "gzip") - }) - invisible() - } - - # Extract the commit hash from a git archive. Git archives include the SHA1 - # hash as the comment field of the tarball pax extended header - # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) - # For GitHub archives this should be the first header after the default one - # (512 byte) header. - renv_bootstrap_git_extract_sha1_tar <- function(bundle) { - - # open the bundle for reading - # We use gzcon for everything because (from ?gzcon) - # > Reading from a connection which does not supply a 'gzip' magic - # > header is equivalent to reading from the original connection - conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) - on.exit(close(conn)) - - # The default pax header is 512 bytes long and the first pax extended header - # with the comment should be 51 bytes long - # `52 comment=` (11 chars) + 40 byte SHA1 hash - len <- 0x200 + 0x33 - res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) - - if (grepl("^52 comment=", res)) { - sub("52 comment=", "", res) - } else { - NULL - } - } - - renv_bootstrap_install <- function(version, tarball, library) { - - # attempt to install it into project library - dir.create(library, showWarnings = FALSE, recursive = TRUE) - output <- renv_bootstrap_install_impl(library, tarball) - - # check for successful install - status <- attr(output, "status") - if (is.null(status) || identical(status, 0L)) - return(status) - - # an error occurred; report it - header <- "installation of renv failed" - lines <- paste(rep.int("=", nchar(header)), collapse = "") - text <- paste(c(header, lines, output), collapse = "\n") - stop(text) - - } - - renv_bootstrap_install_impl <- function(library, tarball) { - - # invoke using system2 so we can capture and report output - bin <- R.home("bin") - exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" - R <- file.path(bin, exe) - - args <- c( - "--vanilla", "CMD", "INSTALL", "--no-multiarch", - "-l", shQuote(path.expand(library)), - shQuote(path.expand(tarball)) - ) - - system2(R, args, stdout = TRUE, stderr = TRUE) - - } - - renv_bootstrap_platform_prefix <- function() { - - # construct version prefix - version <- paste(R.version$major, R.version$minor, sep = ".") - prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") - - # include SVN revision for development versions of R - # (to avoid sharing platform-specific artefacts with released versions of R) - devel <- - identical(R.version[["status"]], "Under development (unstable)") || - identical(R.version[["nickname"]], "Unsuffered Consequences") - - if (devel) - prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") - - # build list of path components - components <- c(prefix, R.version$platform) - - # include prefix if provided by user - prefix <- renv_bootstrap_platform_prefix_impl() - if (!is.na(prefix) && nzchar(prefix)) - components <- c(prefix, components) - - # build prefix - paste(components, collapse = "/") - - } - - renv_bootstrap_platform_prefix_impl <- function() { - - # if an explicit prefix has been supplied, use it - prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) - if (!is.na(prefix)) - return(prefix) - - # if the user has requested an automatic prefix, generate it - auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) - if (is.na(auto) && getRversion() >= "4.4.0") - auto <- "TRUE" - - if (auto %in% c("TRUE", "True", "true", "1")) - return(renv_bootstrap_platform_prefix_auto()) - - # empty string on failure - "" - - } - - renv_bootstrap_platform_prefix_auto <- function() { - - prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) - if (inherits(prefix, "error") || prefix %in% "unknown") { - - msg <- paste( - "failed to infer current operating system", - "please file a bug report at https://github.com/rstudio/renv/issues", - sep = "; " - ) - - warning(msg) - - } - - prefix - - } - - renv_bootstrap_platform_os <- function() { - - sysinfo <- Sys.info() - sysname <- sysinfo[["sysname"]] - - # handle Windows + macOS up front - if (sysname == "Windows") - return("windows") - else if (sysname == "Darwin") - return("macos") - - # check for os-release files - for (file in c("/etc/os-release", "/usr/lib/os-release")) - if (file.exists(file)) - return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) - - # check for redhat-release files - if (file.exists("/etc/redhat-release")) - return(renv_bootstrap_platform_os_via_redhat_release()) - - "unknown" - - } - - renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { - - # read /etc/os-release - release <- utils::read.table( - file = file, - sep = "=", - quote = c("\"", "'"), - col.names = c("Key", "Value"), - comment.char = "#", - stringsAsFactors = FALSE - ) - - vars <- as.list(release$Value) - names(vars) <- release$Key - - # get os name - os <- tolower(sysinfo[["sysname"]]) - - # read id - id <- "unknown" - for (field in c("ID", "ID_LIKE")) { - if (field %in% names(vars) && nzchar(vars[[field]])) { - id <- vars[[field]] - break - } - } - - # read version - version <- "unknown" - for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { - if (field %in% names(vars) && nzchar(vars[[field]])) { - version <- vars[[field]] - break - } - } - - # join together - paste(c(os, id, version), collapse = "-") - - } - - renv_bootstrap_platform_os_via_redhat_release <- function() { - - # read /etc/redhat-release - contents <- readLines("/etc/redhat-release", warn = FALSE) - - # infer id - id <- if (grepl("centos", contents, ignore.case = TRUE)) - "centos" - else if (grepl("redhat", contents, ignore.case = TRUE)) - "redhat" - else - "unknown" - - # try to find a version component (very hacky) - version <- "unknown" - - parts <- strsplit(contents, "[[:space:]]")[[1L]] - for (part in parts) { - - nv <- tryCatch(numeric_version(part), error = identity) - if (inherits(nv, "error")) - next - - version <- nv[1, 1] - break - - } - - paste(c("linux", id, version), collapse = "-") - - } - - renv_bootstrap_library_root_name <- function(project) { - - # use project name as-is if requested - asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") - if (asis) - return(basename(project)) - - # otherwise, disambiguate based on project's path - id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) - paste(basename(project), id, sep = "-") - - } - - renv_bootstrap_library_root <- function(project) { - - prefix <- renv_bootstrap_profile_prefix() - - path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) - if (!is.na(path)) - return(paste(c(path, prefix), collapse = "/")) - - path <- renv_bootstrap_library_root_impl(project) - if (!is.null(path)) { - name <- renv_bootstrap_library_root_name(project) - return(paste(c(path, prefix, name), collapse = "/")) - } - - renv_bootstrap_paths_renv("library", project = project) - - } - - renv_bootstrap_library_root_impl <- function(project) { - - root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) - if (!is.na(root)) - return(root) - - type <- renv_bootstrap_project_type(project) - if (identical(type, "package")) { - userdir <- renv_bootstrap_user_dir() - return(file.path(userdir, "library")) - } - - } - - renv_bootstrap_validate_version <- function(version, description = NULL) { - - # resolve description file - # - # avoid passing lib.loc to `packageDescription()` below, since R will - # use the loaded version of the package by default anyhow. note that - # this function should only be called after 'renv' is loaded - # https://github.com/rstudio/renv/issues/1625 - description <- description %||% packageDescription("renv") - - # check whether requested version 'version' matches loaded version of renv - sha <- attr(version, "sha", exact = TRUE) - valid <- if (!is.null(sha)) - renv_bootstrap_validate_version_dev(sha, description) - else - renv_bootstrap_validate_version_release(version, description) - - if (valid) - return(TRUE) - - # the loaded version of renv doesn't match the requested version; - # give the user instructions on how to proceed - dev <- identical(description[["RemoteType"]], "github") - remote <- if (dev) - paste("rstudio/renv", description[["RemoteSha"]], sep = "@") - else - paste("renv", description[["Version"]], sep = "@") - - # display both loaded version + sha if available - friendly <- renv_bootstrap_version_friendly( - version = description[["Version"]], - sha = if (dev) description[["RemoteSha"]] - ) - - fmt <- heredoc(" - renv %1$s was loaded from project library, but this project is configured to use renv %2$s. - - Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile. - - Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library. - ") - catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) - - FALSE - - } - - renv_bootstrap_validate_version_dev <- function(version, description) { - expected <- description[["RemoteSha"]] - is.character(expected) && startswith(expected, version) - } - - renv_bootstrap_validate_version_release <- function(version, description) { - expected <- description[["Version"]] - is.character(expected) && identical(expected, version) - } - - renv_bootstrap_hash_text <- function(text) { - - hashfile <- tempfile("renv-hash-") - on.exit(unlink(hashfile), add = TRUE) - - writeLines(text, con = hashfile) - tools::md5sum(hashfile) - - } - - renv_bootstrap_load <- function(project, libpath, version) { - - # try to load renv from the project library - if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) - return(FALSE) - - # warn if the version of renv loaded does not match - renv_bootstrap_validate_version(version) - - # execute renv load hooks, if any - hooks <- getHook("renv::autoload") - for (hook in hooks) - if (is.function(hook)) - tryCatch(hook(), error = warnify) - - # load the project - renv::load(project) - - TRUE - - } - - renv_bootstrap_profile_load <- function(project) { - - # if RENV_PROFILE is already set, just use that - profile <- Sys.getenv("RENV_PROFILE", unset = NA) - if (!is.na(profile) && nzchar(profile)) - return(profile) - - # check for a profile file (nothing to do if it doesn't exist) - path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) - if (!file.exists(path)) - return(NULL) - - # read the profile, and set it if it exists - contents <- readLines(path, warn = FALSE) - if (length(contents) == 0L) - return(NULL) - - # set RENV_PROFILE - profile <- contents[[1L]] - if (!profile %in% c("", "default")) - Sys.setenv(RENV_PROFILE = profile) - - profile - - } - - renv_bootstrap_profile_prefix <- function() { - profile <- renv_bootstrap_profile_get() - if (!is.null(profile)) - return(file.path("profiles", profile, "renv")) - } - - renv_bootstrap_profile_get <- function() { - profile <- Sys.getenv("RENV_PROFILE", unset = "") - renv_bootstrap_profile_normalize(profile) - } - - renv_bootstrap_profile_set <- function(profile) { - profile <- renv_bootstrap_profile_normalize(profile) - if (is.null(profile)) - Sys.unsetenv("RENV_PROFILE") - else - Sys.setenv(RENV_PROFILE = profile) - } - - renv_bootstrap_profile_normalize <- function(profile) { - - if (is.null(profile) || profile %in% c("", "default")) - return(NULL) - - profile - - } - - renv_bootstrap_path_absolute <- function(path) { - - substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( - substr(path, 1L, 1L) %in% c(letters, LETTERS) && - substr(path, 2L, 3L) %in% c(":/", ":\\") - ) - - } - - renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { - renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") - root <- if (renv_bootstrap_path_absolute(renv)) NULL else project - prefix <- if (profile) renv_bootstrap_profile_prefix() - components <- c(root, renv, prefix, ...) - paste(components, collapse = "/") - } - - renv_bootstrap_project_type <- function(path) { - - descpath <- file.path(path, "DESCRIPTION") - if (!file.exists(descpath)) - return("unknown") - - desc <- tryCatch( - read.dcf(descpath, all = TRUE), - error = identity - ) - - if (inherits(desc, "error")) - return("unknown") - - type <- desc$Type - if (!is.null(type)) - return(tolower(type)) - - package <- desc$Package - if (!is.null(package)) - return("package") - - "unknown" - - } - - renv_bootstrap_user_dir <- function() { - dir <- renv_bootstrap_user_dir_impl() - path.expand(chartr("\\", "/", dir)) - } - - renv_bootstrap_user_dir_impl <- function() { - - # use local override if set - override <- getOption("renv.userdir.override") - if (!is.null(override)) - return(override) - - # use R_user_dir if available - tools <- asNamespace("tools") - if (is.function(tools$R_user_dir)) - return(tools$R_user_dir("renv", "cache")) - - # try using our own backfill for older versions of R - envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") - for (envvar in envvars) { - root <- Sys.getenv(envvar, unset = NA) - if (!is.na(root)) - return(file.path(root, "R/renv")) - } - - # use platform-specific default fallbacks - if (Sys.info()[["sysname"]] == "Windows") - file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") - else if (Sys.info()[["sysname"]] == "Darwin") - "~/Library/Caches/org.R-project.R/R/renv" - else - "~/.cache/R/renv" - - } - - renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { - sha <- sha %||% attr(version, "sha", exact = TRUE) - parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) - paste(parts, collapse = "") - } - - renv_bootstrap_exec <- function(project, libpath, version) { - if (!renv_bootstrap_load(project, libpath, version)) - renv_bootstrap_run(version, libpath) - } - - renv_bootstrap_run <- function(version, libpath) { - - # perform bootstrap - bootstrap(version, libpath) - - # exit early if we're just testing bootstrap - if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) - return(TRUE) - - # try again to load - if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { - return(renv::load(project = getwd())) - } - - # failed to download or load renv; warn the user - msg <- c( - "Failed to find an renv installation: the project will not be loaded.", - "Use `renv::activate()` to re-initialize the project." - ) - - warning(paste(msg, collapse = "\n"), call. = FALSE) - - } - - renv_json_read <- function(file = NULL, text = NULL) { - - jlerr <- NULL - - # if jsonlite is loaded, use that instead - if ("jsonlite" %in% loadedNamespaces()) { - - json <- tryCatch(renv_json_read_jsonlite(file, text), error = identity) - if (!inherits(json, "error")) - return(json) - - jlerr <- json - - } - - # otherwise, fall back to the default JSON reader - json <- tryCatch(renv_json_read_default(file, text), error = identity) - if (!inherits(json, "error")) - return(json) - - # report an error - if (!is.null(jlerr)) - stop(jlerr) - else - stop(json) - - } - - renv_json_read_jsonlite <- function(file = NULL, text = NULL) { - text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") - jsonlite::fromJSON(txt = text, simplifyVector = FALSE) - } - - renv_json_read_default <- function(file = NULL, text = NULL) { - - # find strings in the JSON - text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") - pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' - locs <- gregexpr(pattern, text, perl = TRUE)[[1]] - - # if any are found, replace them with placeholders - replaced <- text - strings <- character() - replacements <- character() - - if (!identical(c(locs), -1L)) { - - # get the string values - starts <- locs - ends <- locs + attr(locs, "match.length") - 1L - strings <- substring(text, starts, ends) - - # only keep those requiring escaping - strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) - - # compute replacements - replacements <- sprintf('"\032%i\032"', seq_along(strings)) - - # replace the strings - mapply(function(string, replacement) { - replaced <<- sub(string, replacement, replaced, fixed = TRUE) - }, strings, replacements) - - } - - # transform the JSON into something the R parser understands - transformed <- replaced - transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) - transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) - transformed <- gsub("[]}]", ")", transformed, perl = TRUE) - transformed <- gsub(":", "=", transformed, fixed = TRUE) - text <- paste(transformed, collapse = "\n") - - # parse it - json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] - - # construct map between source strings, replaced strings - map <- as.character(parse(text = strings)) - names(map) <- as.character(parse(text = replacements)) - - # convert to list - map <- as.list(map) - - # remap strings in object - remapped <- renv_json_read_remap(json, map) - - # evaluate - eval(remapped, envir = baseenv()) - - } - - renv_json_read_remap <- function(json, map) { - - # fix names - if (!is.null(names(json))) { - lhs <- match(names(json), names(map), nomatch = 0L) - rhs <- match(names(map), names(json), nomatch = 0L) - names(json)[rhs] <- map[lhs] - } - - # fix values - if (is.character(json)) - return(map[[json]] %||% json) - - # handle true, false, null - if (is.name(json)) { - text <- as.character(json) - if (text == "true") - return(TRUE) - else if (text == "false") - return(FALSE) - else if (text == "null") - return(NULL) - } - - # recurse - if (is.recursive(json)) { - for (i in seq_along(json)) { - json[i] <- list(renv_json_read_remap(json[[i]], map)) - } - } - - json - - } - - # load the renv profile, if any - renv_bootstrap_profile_load(project) - - # construct path to library root - root <- renv_bootstrap_library_root(project) - - # construct library prefix for platform - prefix <- renv_bootstrap_platform_prefix() - - # construct full libpath - libpath <- file.path(root, prefix) - - # run bootstrap code - renv_bootstrap_exec(project, libpath, version) - - invisible() - -}) diff --git a/renv/settings.json b/renv/settings.json deleted file mode 100644 index dec23cf4..00000000 --- a/renv/settings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "bioconductor.version": null, - "external.libraries": [], - "ignored.packages": [], - "package.dependency.fields": [ - "Imports", - "Depends", - "LinkingTo" - ], - "ppm.enabled": true, - "ppm.ignored.urls": [], - "r.version": null, - "snapshot.type": "implicit", - "use.cache": true, - "vcs.ignore.cellar": true, - "vcs.ignore.library": true, - "vcs.ignore.local": true, - "vcs.manage.ignores": true -} From aa46610bbb1b4b5ff0cc18b3695c6f0028365948 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Tue, 30 Sep 2025 14:05:49 -0500 Subject: [PATCH 5/6] Rename straggling references to pinval -> homeval --- README.md | 4 ++-- scripts/generate_homeval/uv.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ff7b7be6..3cff100c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ hugo serve ``` 3. Any reports that [you have - generated](https://github.com/ccao-data/homeval/blob/main/scripts/generate_pinval/generate_pinval.py) + generated](https://github.com/ccao-data/homeval/blob/main/scripts/generate_homeval/generate_homeval.py) will be available in a browser at http://localhost:1313/. ### Generating reports @@ -62,6 +62,6 @@ documentation for the script. ## Deployment We manage deployments using the [`generate-homeval` GitHub -workflow](https://github.com/ccao-data/homeval/actions/workflows/generate-pinval.yaml). +workflow](https://github.com/ccao-data/homeval/actions/workflows/generate-homeval.yaml). That workflow includes options for deploying to a development environment as well as the production environment. diff --git a/scripts/generate_homeval/uv.lock b/scripts/generate_homeval/uv.lock index 09784ba9..e8f94598 100644 --- a/scripts/generate_homeval/uv.lock +++ b/scripts/generate_homeval/uv.lock @@ -52,7 +52,7 @@ wheels = [ ] [[package]] -name = "generate-pinval" +name = "generate-homeval" version = "0.1.0" source = { editable = "." } dependencies = [ From 5d4394debb3407f6d00872030570daf6d7cf20fc Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Tue, 30 Sep 2025 15:41:01 -0500 Subject: [PATCH 6/6] Fix links in README --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3cff100c..5f0cf469 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,12 @@ cd hugo hugo serve ``` -3. Any reports that [you have - generated](https://github.com/ccao-data/homeval/blob/main/scripts/generate_homeval/generate_homeval.py) +3. Any reports that [you have generated](#generating-reports) will be available in a browser at http://localhost:1313/. ### Generating reports -You can use the [`scripts/generate_homeval` +You can use the [`generate_homeval` script](https://github.com/ccao-data/homeval/blob/main/scripts/generate_homeval/generate_homeval.py) to generate reports that you can view locally.