Skip to content

[Data] Refactor br_tse_eleicoes#1476

Open
rdahis wants to merge 74 commits into
mainfrom
feat/refactor_eleicoes
Open

[Data] Refactor br_tse_eleicoes#1476
rdahis wants to merge 74 commits into
mainfrom
feat/refactor_eleicoes

Conversation

@rdahis

@rdahis rdahis commented Mar 24, 2026

Copy link
Copy Markdown
Member

Descrição

PR para refatorar o código de limpeza dos dados de eleições de Stata para Python. O código foi escrito com auxílio do Claude Code.

Substitui o PR #1467.

Detalhes Técnicos:

  • O processo envolveu dar de inputs todo o código de Stata, os arquivos brutos em /input e tudo já gerado pelo Stata em /output. Fomos escrevendo o código para gerar precisamente o mesmo output a partir do código refatorado em Python. Rodamos diversos testes de validação (ex: ordem das colunas, número de linhas, médias, valores únicos).
  • O código em Python gera arquivos formato .parquet, que são depois normalizados e particionados em .csv.

Requerimentos

  • Ter todos os arquivos /input disponíveis. Hoje eu mantenho tudo no meu Dropbox local.
  • É necessário ~400 GB de espaço local para rodar e validar tudo.

Summary by CodeRabbit

  • Documentation
    • Added a detailed end-to-end refactoring plan for rewriting the election ETL pipeline in Python.
    • Expanded pipeline documentation (dependency map) and added ignore rules for validation artifacts.
  • New Features
    • Introduced a Python-based ETL that generates normalized election datasets with Hive-style partitioned outputs.
    • Added a dedicated aggregation stage for municipality and national rollups.
    • Added an output parity validator to compare Python results against legacy references.
  • Refactor
    • Rebuilt the full pipeline flow with standardized cleaning, schemas, and partitioning behavior.
  • Chores
    • Removed the previous notebook-based implementation.

rdahis added 4 commits March 20, 2026 10:58
…tions (Phases 0-1)

Phase 0 -- Infrastructure: config.py (paths, year ranges, state lists, null
sentinels), utils/helpers.py (read_raw_csv, clean_nulls, parse_date_br,
pad_cpf, pad_titulo, merge_municipio, save_partitioned), and validate.py
for comparing Python parquet outputs against Stata .dta references.

Phase 1 -- Cleaning functions ported from Stata fnc/ to Python utils/:
clean_string, clean_election_type, clean_education, clean_marital_status,
clean_result, clean_party, fix_candidate. Data values remain in Portuguese
to match the source data; file and function names are in English.
…les)

Implements 13 sub/ modules translating each Stata .do table builder to Python:
- candidates, parties, vacancies
- voter_profile_mun_zone, voter_profile_section, voter_profile_polling_place
- voting_details_mun_zone, voting_details_section, voting_details_state (1945-1990)
- results_mun_zone, results_section, results_state (1945-1990)
- campaign_finance (bens 2006+, receitas 2002+, despesas 2002+)
- clean_election_type: map 'eleicao YYYY' → 'eleicao ordinaria' for all
  years (previously only applied for ano > 1982)
- voting_details_state: guard proportion calculations against div-by-zero
- results_mun_zone: fix id_municipio column order for candidato 1996-2016
  (1994 uses natural merge order; partido fix already applied)
- results_section: fix partido merge key regression that produced _x/_y
  suffix columns; enforce column order to match Stata output
- results_state: fix apostrophe/accent casing after str.title(); add
  column order; fix file-path patterns and encoding (latin-1)
- candidates/parties/voter_profile_section: fix title-case accent
  handling, column ordering, and encoding issues
- campaign_finance: fix multiline truncation, quote stripping, encoding
- helpers: default encoding latin-1 for TSE raw CSV files
- validate: improve float normalization and column comparison logic
- remove stale [dbt] notebook
@rdahis rdahis requested a review from a team March 24, 2026 05:18
@rdahis rdahis self-assigned this Mar 24, 2026
@rdahis rdahis added the data Nova base, conjunto ou tabela a ser adicionada à BD label Mar 24, 2026
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a modular Python ETL for br_tse_eleicoes: centralized config, per-table/year builders (candidates, parties, results, voting details, voter profiles, campaign finance), normalization/partitioning, aggregation, and a validator; includes a PLAN and removal of an older Jupyter notebook.

Changes

Documentation & Planning

Layer / File(s) Summary
Plan & roadmap
models/br_tse_eleicoes/code/PLAN.md
End-to-end refactoring plan specifying target Python structure, pipeline phases, validation strategies, and implementation status across scaffolding, building, normalization, aggregation, and orchestration.

Removed

Layer / File(s) Summary
Deleted Jupyter notebook
models/br_tse_eleicoes/code/[dbt]br_tse_eleicoes.ipynb
Removed notebook containing prior data-processing logic, utilities, and end-to-end workflows (cells and outputs deleted).

Core Pipeline Orchestration

Layer / File(s) Summary
Orchestration, config, validate, normalization, aggregation
models/br_tse_eleicoes/code/python/build.py, models/br_tse_eleicoes/code/python/config.py, models/br_tse_eleicoes/code/python/validate.py, models/br_tse_eleicoes/code/python/normalization_partition.py, models/br_tse_eleicoes/code/python/aggregation.py
Pipeline entrypoint with step registry and CLI; centralized paths, year/UFs, and null sentinels; validation comparing Python parquet outputs to Stata .dta; normalization/partitioning and aggregation flows creating Hive-style outputs.

Table-Specific Builders

Layer / File(s) Summary
Results and core tables
models/br_tse_eleicoes/code/python/sub/candidates.py, .../parties.py, .../results_mun_zone.py, .../results_section.py, .../results_state.py, .../vacancies.py
Per-table builders implementing year-range schema mappings, normalization, numeric coercion, deduplication, and writing parquet outputs.

Voter Profile Builders

Layer / File(s) Summary
Voter profile datasets
models/br_tse_eleicoes/code/python/sub/voter_profile_mun_zone.py, .../voter_profile_polling_place.py, .../voter_profile_section.py
Builders for voter profile data at municipality-zone, polling-place, and section levels with year-aware column handling and municipio merging.

Voting Details Builders

Layer / File(s) Summary
Voting/turnout details
models/br_tse_eleicoes/code/python/sub/voting_details_mun_zone.py, .../voting_details_section.py, .../voting_details_state.py
Builders for voting details at municipio-zone, section, and historical state (1945–1990) levels; compute proportions, reconcile duplicates, and produce partitioned outputs.

Campaign Finance Builder

Layer / File(s) Summary
Campaign finance (bens/receitas/despesas)
models/br_tse_eleicoes/code/python/sub/campaign_finance.py
Comprehensive builders for assets, receipts, and expenses across 2002–2024 with year-specific schema branching, UF coverage rules, null/token handling, date/currency parsing, CPF/CNPJ padding, municipio merging, and parquet persistence.

Utility Modules

Layer / File(s) Summary
Helpers and cleaners
models/br_tse_eleicoes/code/python/utils/helpers.py, clean_education.py, clean_election_type.py, clean_marital_status.py, clean_party.py, clean_result.py, clean_string.py, fix_candidate.py
Shared CSV readers, null-sentinel replacement, date/CPF/título parsing and padding, municipio directory loading/merge, Hive-style partitioned CSV writer, and domain-specific cleaning utilities.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

test-dev-model, table-approve

Suggested reviewers

  • folhesgabriel

Poem

🐰 I found the datasets in a maze of rows,
I hopped and parsed where old Stata flows.
Dates and CPFs all neatly spun,
Partitions baked and outputs done.
A little carrot for pipelines well-run. 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers motivation (migrating Stata code to Python), technical details (validation approach, file formats), and operational requirements, but lacks details on testing status, risks/mitigation, and explicit dependency declarations as outlined in the template. Complete the testing section (check the relevant boxes for local/cloud testing), add a 'Risks and Mitigations' section addressing the 400 GB requirement and rollback strategy, and clarify the 'Dependencies' section status.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Data] Refactor br_tse_eleicoes' clearly identifies this as a data-related refactoring of the TSE elections pipeline, following the repository's naming convention with the [Data] keyword and specific project name.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/refactor_eleicoes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (15)
models/br_tse_eleicoes/code/python/utils/clean_party.py-8-13 (1)

8-13: ⚠️ Potential issue | 🟠 Major

Add docstring to clean_party() and fix mapping order for party variants.

The clean_party() function lacks a docstring. Additionally, year-specific mapping is applied before always-mapping, which causes inconsistency: variants like "PTdoB" and "PT DO B" are normalized to "PT do B" via _ALWAYS_MAP, but this happens after year-specific mapping, so they won't be converted to "AVANTE" in 2014/2016 even though "PT do B" is. To ensure all variants map consistently, apply _ALWAYS_MAP before _YEAR_MAP.

Also, clean_party_series() is missing type hints: add type annotation for parameter s and return type (should be pandas.Series).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_party.py` around lines 8 - 13,
Add a docstring to clean_party() describing its purpose, inputs and outputs;
change the normalization order so that _ALWAYS_MAP is applied before _YEAR_MAP
(i.e., normalize variants via _ALWAYS_MAP first, then apply year-specific
remapping using _YEAR_MAP) to ensure variants like "PTdoB"/"PT DO B" resolve to
"AVANTE" for 2014/2016; and add type hints to clean_party_series(s:
pandas.Series) -> pandas.Series for the function signature.
models/br_tse_eleicoes/code/python/utils/fix_candidate.py-14-18 (1)

14-18: ⚠️ Potential issue | 🟠 Major

Fix type mismatch: ano column is numeric, not string.

Both masks compare df["ano"] to string literals ("2000" and "2006"), but upstream code confirms ano is stored as numeric (int). Comparisons like df["ano"] % 2 and df["ano"] = 201407 in voting processing modules prove the column type. String comparisons to numeric values will silently yield no matches, preventing the fixes from being applied.

Update both comparisons to convert the column to string:

Suggested fix
     mask = (
-        (df["ano"] == "2000")
+        (df["ano"].astype(str) == "2000")
         & (df["id_municipio"] == "4202909")
         & (df["nome"] == "Paulo Peixer")
     )

Apply the same fix to the second mask at lines 26-29 for "2006".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/fix_candidate.py` around lines 14 -
18, The mask currently compares df["ano"] to the string "2000" (and another mask
compares to "2006"), but ano is numeric; update both masks so they compare the
string form of the year by calling df["ano"].astype(str) (e.g., replace
df["ano"] == "2000" with df["ano"].astype(str) == "2000") to ensure matches for
the mask variables that assign to mask (and the second mask that checks "2006").
models/br_tse_eleicoes/code/python/validate.py-19-32 (1)

19-32: ⚠️ Potential issue | 🟠 Major

The validator currently skips outputs that are written once for all years.

Both loaders and discover_tables() only understand {table}_{year} artifacts. That means datasets like perfil_eleitorado_local_votacao.parquet from models/br_tse_eleicoes/code/python/sub/voter_profile_polling_place.py never enter validation, so the parity check is incomplete for at least one generated table.

Also applies to: 204-212, 259-267

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/validate.py` around lines 19 - 32, The
loaders (load_stata_dta, load_python_parquet) and discover_tables() only look
for files named with the "{table}_{year}" pattern, so year-agnostic artifacts
(e.g., perfil_eleitorado_local_votacao.parquet) are skipped; update
load_stata_dta and load_python_parquet to fallback to a yearless filename when
the yeared path doesn't exist (check OUTPUT_STATA / f"{table}.dta" and
OUTPUT_PYTHON / f"{table}.parquet"), and update discover_tables() to include
files that match either the "{table}_{year}" pattern or the standalone "{table}"
pattern so those generated tables are discovered and validated.
models/br_tse_eleicoes/code/python/validate.py-179-195 (1)

179-195: ⚠️ Potential issue | 🟠 Major

Report numeric nullability mismatches even when one side is all-null.

The if ref_nn > 0 and test_nn > 0: guard suppresses cases like ref_nn == 0 and test_nn > 0, so a materially different numeric column can still produce no numeric issue. Compare non-null counts unconditionally, then skip only the sum check when both sides are fully null.

🐛 Proposed fix
         ref_nn = ref_num.notna().sum()
         test_nn = test_num.notna().sum()
-        if ref_nn > 0 and test_nn > 0:
-            if ref_nn != test_nn:
-                issues.append(
-                    f"[{label}] Column '{col}' numeric count: ref={ref_nn}, test={test_nn}"
-                )
-            ref_sum = ref_num.sum()
-            test_sum = test_num.sum()
-            if not np.isclose(ref_sum, test_sum, rtol=1e-6, equal_nan=True):
-                issues.append(
-                    f"[{label}] Column '{col}' sum: ref={ref_sum}, test={test_sum}"
-                )
+        if ref_nn != test_nn:
+            issues.append(
+                f"[{label}] Column '{col}' numeric count: ref={ref_nn}, test={test_nn}"
+            )
+        if ref_nn == 0 and test_nn == 0:
+            continue
+        ref_sum = ref_num.sum()
+        test_sum = test_num.sum()
+        if not np.isclose(ref_sum, test_sum, rtol=1e-6, equal_nan=True):
+            issues.append(
+                f"[{label}] Column '{col}' sum: ref={ref_sum}, test={test_sum}"
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/validate.py` around lines 179 - 195, In
the numeric-column loop that iterates over float_cols + numeric_cols, always
compare and report non-null count mismatches by computing ref_nn and test_nn and
appending the issues entry when ref_nn != test_nn; only guard the sum comparison
(ref_sum/test_sum using ref_num/test_num) behind a conditional that both ref_nn
> 0 and test_nn > 0 so you skip sum checks when either side is all-null. Update
the block around ref_num/test_num, ref_nn/test_nn, and the issues.append calls
to enforce unconditional count checks but conditional sum checks.
models/br_tse_eleicoes/code/python/sub/voter_profile_section.py-120-157 (1)

120-157: ⚠️ Potential issue | 🟠 Major

Normalize ano before grouping, or 2014 section data will keep the bad 201407 key.

This builder never destrings ano, so it also never applies the 201407 -> 2014 correction that the municipality-zone builder already carries. Grouping on the raw value here will emit 2014 rows under the wrong year and break year-keyed joins/validation for the section output.

🐛 Proposed fix
         for col in [
+            "ano",
             "id_municipio_tse",
             "zona",
             "secao",
             "eleitores",
             "eleitores_biometria",
             "eleitores_deficiencia",
             "eleitores_inclusao_nome_social",
         ]:
             if col in df.columns:
                 df[col] = pd.to_numeric(df[col], errors="coerce")
+
+        df.loc[df["ano"] == 201407, "ano"] = 2014
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voter_profile_section.py` around lines
120 - 157, The grouping uses the raw 'ano' column so rows like 201407 remain and
break year-keyed joins; before building group_cols and grouping on df,
coerce/destring the 'ano' column (e.g., pd.to_numeric(df['ano'],
errors='coerce')) and normalize the 201407 case to 2014 (apply the same 201407
-> 2014 correction used in the municipality-zone builder) so group_cols/groupby
uses the corrected year; update references around df, group_cols, and sum_cols
to ensure 'ano' is in numeric/normalized form prior to df.groupby.
models/br_tse_eleicoes/code/python/config.py-12-17 (1)

12-17: ⚠️ Potential issue | 🟠 Major

Don't bake one developer's filesystem into the checked-in config.

RAW_DATA points at /Users/rdahis/Downloads/dados_TSE, so every builder and validator imported from this module will fail anywhere except that machine. Please make the data root configurable via env var / CLI / local config instead of a repo-committed absolute path.

💡 Portable configuration sketch
+import os
 from pathlib import Path
@@
-RAW_DATA = Path("/Users/rdahis/Downloads/dados_TSE")
+RAW_DATA = Path(
+    os.environ.get("BR_TSE_ELEICOES_RAW_DATA", "dados_TSE")
+).expanduser()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/config.py` around lines 12 - 17, The
config currently hardcodes RAW_DATA = Path("/Users/rdahis/Downloads/dados_TSE")
which breaks on other machines; change RAW_DATA to be configurable (e.g., read
from an environment variable like os.getenv("BR_TSE_RAW_DATA") with a sensible
default) and update dependent symbols INPUT_DIR, OUTPUT_STATA, OUTPUT_PYTHON,
and MUNICIPIO_DIR_CSV to derive from that value; ensure the module falls back to
a relative repo path if the env var is not set and document the env var name in
a comment so local devs can override without editing the file.
models/br_tse_eleicoes/code/python/sub/parties.py-27-36 (1)

27-36: ⚠️ Potential issue | 🟠 Major

Don't let malformed party rows get silently dropped.

The on_bad_lines="warn" parameter skips bad records without including them in the DataFrame, creating silent data loss. Since this file is explicitly designed to match Stata output exactly (as noted in the module docstring: "Equivalent of sub/partidos.do"), dropping records without failing breaks that equivalence goal. Either fail fast on bad input or explicitly quarantine problematic files instead.

🔧 Safer default
                 return pd.read_csv(
                     path,
                     sep=";",
                     header=None,
                     dtype=str,
                     encoding="latin-1",
                     keep_default_na=False,
                     quotechar='"',
-                    on_bad_lines="warn",
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/parties.py` around lines 27 - 36, The
current pd.read_csv call in models/br_tse_eleicoes/code/python/sub/parties.py
uses on_bad_lines="warn" which silently drops malformed rows; change this to
fail fast by using on_bad_lines="error" so the exception propagates, or
implement an on_bad_lines callable that writes each malformed line to a
quarantine file (e.g., via a handle_bad_line(line) callback) and then raises an
exception to stop processing—update the pd.read_csv invocation accordingly and
ensure any raised error is not swallowed so malformed party rows are not
silently lost.
models/br_tse_eleicoes/code/python/sub/results_state.py-60-61 (1)

60-61: ⚠️ Potential issue | 🟠 Major

Move historical sentinel cleanup before pd.to_numeric().

_HIST_NULLS includes "-1" and "-3", but both builders coerce numeric columns first. After that, those sentinels are numeric -1/-3 and _clean_hist_nulls() no longer removes them, so negative vote counts can leak through.

🔧 Suggested fix
-        # destring
-        for col in ["ano", "turno", "votos"]:
-            df[col] = pd.to_numeric(df[col], errors="coerce")
-
-        # clean nulls
-        df = _clean_hist_nulls(df)
+        # clean nulls before destring so "-1"/"-3" are removed
+        df = _clean_hist_nulls(df)
+        for col in ["ano", "turno", "votos"]:
+            df[col] = pd.to_numeric(df[col], errors="coerce")

Apply the same reorder in build_partido() for votos_nominais and votos_nao_nominais.

Also applies to: 207-212, 319-324

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/results_state.py` around lines 60 -
61, The historical sentinel set _HIST_NULLS contains string tokens like "-1" and
"-3" which get coerced to numeric -1/-3 by pd.to_numeric, so update the data
cleaning order: call _clean_hist_nulls(...) to remove those string sentinels
before any pd.to_numeric conversions. Specifically modify build_partido() (and
the other builders that handle votos_nominais and votos_nao_nominais) to perform
_clean_hist_nulls on those columns first, then call pd.to_numeric on the cleaned
columns, and ensure _HIST_NULLS and _clean_hist_nulls() are referenced for
clarity.
models/br_tse_eleicoes/code/python/sub/candidates.py-174-176 (1)

174-176: ⚠️ Potential issue | 🟠 Major

fix_candidate() never matches after ano is coerced to numeric.

models/br_tse_eleicoes/code/python/utils/fix_candidate.py compares df["ano"] against the string years "2000" and "2006". By the time Line 230 calls it, ano is numeric, so those manual CPF/título fixes never fire.

🔧 Suggested fix in models/br_tse_eleicoes/code/python/utils/fix_candidate.py
ano = pd.to_numeric(df["ano"], errors="coerce").astype("Int64")

mask = (
    (ano == 2000)
    & (df["id_municipio"] == "4202909")
    & (df["nome"] == "Paulo Peixer")
)

mask2 = (
    (ano == 2006)
    & (df["sigla_uf"] == "SP")
    & (df["nome"] == "José Carlos Selbach Eymael")
)

Also applies to: 225-230

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/candidates.py` around lines 174 - 176,
The fix_candidate routine is comparing df["ano"] to string years, but earlier
code coerces ano to numeric so those masks never match; in fix_candidate.py
convert or coerce df["ano"] to a numeric/nullable Int (e.g., via
pd.to_numeric(...).astype("Int64")) and then update the masks in fix_candidate
(the comparisons inside fix_candidate.py that reference ano, e.g., the masks
matching (ano == 2000) and (ano == 2006)) to compare against integer literals
rather than strings; ensure you reference the same column names used in the
function (df["ano"], df["id_municipio"] or df["id_municipio_tse"] as
appropriate, df["sigla_uf"], df["nome"]) so the CPF/título fixes execute.
models/br_tse_eleicoes/code/python/sub/voting_details_mun_zone.py-305-317 (1)

305-317: ⚠️ Potential issue | 🟠 Major

Protect the percentage calculations from zero turnout/eligibility.

Dividing by raw aptos and comparecimento can produce invalid percentages for empty zones. Reuse the same zero-to-null guard used in voting_details_state.py.

🔧 Suggested fix
+        aptos_safe = df["aptos"].replace(0, pd.NA)
+        comparecimento_safe = df["comparecimento"].replace(0, pd.NA)
         df["proporcao_comparecimento"] = (
-            100 * df["comparecimento"] / df["aptos"]
+            100 * df["comparecimento"] / aptos_safe
         )
         df["proporcao_votos_validos"] = (
-            100 * df["votos_validos"] / df["comparecimento"]
+            100 * df["votos_validos"] / comparecimento_safe
         )
         df["proporcao_votos_brancos"] = (
-            100 * df["votos_brancos"] / df["comparecimento"]
+            100 * df["votos_brancos"] / comparecimento_safe
         )
         df["proporcao_votos_nulos"] = (
-            100 * df["votos_nulos"] / df["comparecimento"]
+            100 * df["votos_nulos"] / comparecimento_safe
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voting_details_mun_zone.py` around
lines 305 - 317, The percentage columns (proporcao_comparecimento,
proporcao_votos_validos, proporcao_votos_brancos, proporcao_votos_nulos)
currently divide by raw 'aptos' and 'comparecimento' and can produce inf/NaN for
empty zones; update these calculations to guard against zero denominators like
the implementation in voting_details_state.py by using the same zero-to-null
pattern (e.g. replace 0 denominators with NaN or use conditional masking) so
that divisions only occur when 'aptos' or 'comparecimento' > 0 and otherwise set
the percentage to null.
models/br_tse_eleicoes/code/python/sub/voting_details_section.py-145-160 (1)

145-160: ⚠️ Potential issue | 🟠 Major

Guard zero denominators in the ratio columns.

Rows with aptos == 0 or comparecimento == 0 will emit invalid percentages here instead of nulls. The state-level builder already protects this path, so this diverges on edge cases.

🔧 Suggested fix
+        aptos_safe = df["aptos"].replace(0, pd.NA)
+        comparecimento_safe = df["comparecimento"].replace(0, pd.NA)
         df["proporcao_comparecimento"] = (
-            100 * df["comparecimento"] / df["aptos"]
+            100 * df["comparecimento"] / aptos_safe
         )
         df["proporcao_votos_nominais"] = (
-            100 * df["votos_nominais"] / df["comparecimento"]
+            100 * df["votos_nominais"] / comparecimento_safe
         )
         df["proporcao_votos_legenda"] = (
-            100 * df["votos_legenda"] / df["comparecimento"]
+            100 * df["votos_legenda"] / comparecimento_safe
         )
         df["proporcao_votos_brancos"] = (
-            100 * df["votos_brancos"] / df["comparecimento"]
+            100 * df["votos_brancos"] / comparecimento_safe
         )
         df["proporcao_votos_nulos"] = (
-            100 * df["votos_nulos"] / df["comparecimento"]
+            100 * df["votos_nulos"] / comparecimento_safe
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voting_details_section.py` around
lines 145 - 160, The ratio columns (proporcao_comparecimento,
proporcao_votos_nominais, proporcao_votos_legenda, proporcao_votos_brancos,
proporcao_votos_nulos) currently divide by df["aptos"] or df["comparecimento"]
without guarding zero denominators; update the computation so divisions produce
NaN (or None) when the denominator is zero — e.g., compute
proporcao_comparecimento only when df["aptos"] > 0 and compute the other
proporcoes only when df["comparecimento"] > 0 using a conditional expression
(pd.Series.where, np.where, or .mask) on df to set results to NaN for zero
denominators; modify the expressions that create these five columns in
voting_details_section.py accordingly and keep column names the same.
models/br_tse_eleicoes/code/python/utils/helpers.py-63-77 (1)

63-77: ⚠️ Potential issue | 🟠 Major

Narrow exception handler to ParserError only.

Catching Exception masks unrelated IO/schema errors and converts them to silent data skipping with on_bad_lines="skip". The fallback should only handle actual parser failures (like the SP 2014 truncated quote case), not all exceptions.

🔧 Suggested fix
-    except (pd.errors.ParserError, Exception):
+    except pd.errors.ParserError:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/helpers.py` around lines 63 - 77,
The except block currently catches (pd.errors.ParserError, Exception) which
masks unrelated errors; change it to catch only pd.errors.ParserError so only
parser failures trigger the fallback read using engine="python" and
on_bad_lines="skip", and ensure any other exceptions are not swallowed (i.e.,
let other exceptions propagate instead of being handled by the fallback). Locate
the except line that wraps the fallback pd.read_csv call (the block that uses
path, encoding, quotechar='"', engine="python", on_bad_lines="skip") and replace
the broad exception tuple with a single pd.errors.ParserError.
models/br_tse_eleicoes/code/python/sub/campaign_finance.py-1596-1606 (1)

1596-1606: ⚠️ Potential issue | 🟠 Major

Normalize data_despesa in the 2012 despesas builder.

This branch patches eight-character values but never performs the final date normalization step, so 2012 becomes the only despesas year that emits data_despesa in a different format than the rest of the table.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/campaign_finance.py` around lines 1596
- 1606, The code prepends "20" for 8-char 2012 dates but never applies the final
date normalization, leaving 2012 values in a different format; after the lambda
that updates df["data_despesa"], run the same normalization used elsewhere for
despesas (e.g., call the shared date-normalizer or apply pd.to_datetime with the
project’s format and errors="coerce") on df["data_despesa"] so all branches
produce consistent date types; locate this change near the lambda applying to
df["data_despesa"] in the 2012 despesas builder and mirror the normalization
step used by other years.
models/br_tse_eleicoes/code/python/normalization_partition.py-894-905 (1)

894-905: ⚠️ Potential issue | 🟠 Major

Filter perfil_eleitorado_secao by _UFS_PERFIL_SECAO[ano] before partitioning.

The year-specific UF map is currently dead code. This loop partitions every sigla_uf present in the parquet, so DF, ZZ, or other extra partitions can leak into years that should not publish them.

Suggested fix
     print("  partitioning perfil_eleitorado_secao...")
     for ano in sorted(_UFS_PERFIL_SECAO.keys()):
         df = _read_parquet("perfil_eleitorado_secao", ano)
         if df.empty:
             continue
+        allowed_ufs = set(_UFS_PERFIL_SECAO[ano])
+        df = df[df["sigla_uf"].isin(allowed_ufs)].copy()
         save_partitioned(
             df,
             "perfil_eleitorado_secao",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/normalization_partition.py` around lines
894 - 905, The loop currently partitions all sigla_uf present in the parquet for
perfil_eleitorado_secao, ignoring the per-year whitelist _UFS_PERFIL_SECAO;
before calling save_partitioned you should filter the dataframe to only rows
whose "sigla_uf" is in _UFS_PERFIL_SECAO[ano] (e.g., df =
df[df["sigla_uf"].isin(_UFS_PERFIL_SECAO[ano])]) and skip/save only when that
filtered df is non-empty; update the code around the perfil_eleitorado_secao
handling that uses _read_parquet("perfil_eleitorado_secao", ano) and
save_partitioned(...) to apply this filter so extraneous UFs (DF, ZZ, etc.) are
not written for years that shouldn't publish them.
models/br_tse_eleicoes/code/python/aggregation.py-450-475 (1)

450-475: ⚠️ Potential issue | 🟠 Major

Guard the recomputed proportions against zero denominators.

aptos and comparecimento can be zero after the municipality rollup. Pandas will emit inf/NaN here, which then gets written to the published CSVs.

Zero-safe division sketch
-            if "comparecimento" in agg.columns and "aptos" in agg.columns:
-                agg["proporcao_comparecimento"] = (
-                    100 * agg["comparecimento"] / agg["aptos"]
-                )
+            if "comparecimento" in agg.columns:
+                comparecimento_den = agg["comparecimento"].replace(0, pd.NA)
+            if "comparecimento" in agg.columns and "aptos" in agg.columns:
+                aptos_den = agg["aptos"].replace(0, pd.NA)
+                agg["proporcao_comparecimento"] = (
+                    100 * agg["comparecimento"] / aptos_den
+                )
             if (
                 "votos_validos" in agg.columns
                 and "comparecimento" in agg.columns
             ):
                 agg["proporcao_votos_validos"] = (
-                    100 * agg["votos_validos"] / agg["comparecimento"]
+                    100 * agg["votos_validos"] / comparecimento_den
                 )
             if (
                 "votos_brancos" in agg.columns
                 and "comparecimento" in agg.columns
             ):
                 agg["proporcao_votos_brancos"] = (
-                    100 * agg["votos_brancos"] / agg["comparecimento"]
+                    100 * agg["votos_brancos"] / comparecimento_den
                 )
             if (
                 "votos_nulos" in agg.columns
                 and "comparecimento" in agg.columns
             ):
                 agg["proporcao_votos_nulos"] = (
-                    100 * agg["votos_nulos"] / agg["comparecimento"]
+                    100 * agg["votos_nulos"] / comparecimento_den
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/aggregation.py` around lines 450 - 475,
When recomputing proportion columns on DataFrame agg (proporcao_comparecimento,
proporcao_votos_validos, proporcao_votos_brancos, proporcao_votos_nulos), guard
against zero denominators (aptos and comparecimento) by performing zero-safe
division: compute each proportion only where the denominator is > 0 and
otherwise set a safe value (e.g., 0 or NaN) using a conditional mask or
numpy.where / pandas.Series.where; update the calculations for
proporcao_comparecimento to check agg["aptos"] > 0 and for the other three to
check agg["comparecimento"] > 0 so no inf/NaN values are produced and written to
CSV.
🧹 Nitpick comments (17)
models/br_tse_eleicoes/code/python/utils/clean_education.py (1)

27-33: Add missing type hints and docstring per coding guidelines.

clean_education lacks a docstring, and clean_education_series lacks type hints.

Suggested fix
 def clean_education(val: str) -> str:
+    """Return the canonical education level for known variants, else unchanged."""
     return _MAP.get(val, val)
 
 
-def clean_education_series(s):
+def clean_education_series(s: "pd.Series") -> "pd.Series":
     """Apply clean_education to a pandas Series."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_education.py` around lines 27
- 33, Add a docstring to clean_education explaining what mappings _MAP contains
and that the function returns the cleaned/normalized education string (input:
val: str -> output: str), and add explicit type hints to clean_education_series
so its signature reads something like clean_education_series(s: pd.Series) ->
pd.Series; update the docstring on clean_education_series to describe it applies
clean_education element-wise to a pandas Series (preserving non-str values), and
ensure pandas is imported as pd if not already.
models/br_tse_eleicoes/code/python/utils/clean_string.py (2)

33-36: Missing û and Û in accent map.

The map handles ù and ü for lowercase (and Ù, Ü for uppercase), but û (u with circumflex) and Û are absent. While rare in Portuguese, they can appear in names of foreign origin.

Suggested fix
         "ú": "u",
         "ù": "u",
+        "û": "u",
         "ü": "u",
         "ç": "c",

And for uppercase:

         "Ú": "U",
         "Ù": "U",
+        "Û": "U",
         "Ü": "U",
         "Ç": "C",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_string.py` around lines 33 -
36, The accent replacement map in clean_string.py is missing the lowercase 'û'
and uppercase 'Û'; update the mapping (the accent map used by the clean_string
utilities, e.g., the variable that currently contains "ú","ù","ü","ç", etc.) to
include "û": "u" and the corresponding uppercase entry "Û": "U" in the uppercase
section so both lowercase and uppercase circumflex-u are normalized correctly.

89-91: Add type hints for clean_string_series.

Missing type hints for parameter s and return type per coding guidelines.

Suggested fix
-def clean_string_series(s):
+def clean_string_series(s: "pd.Series") -> "pd.Series":
     """Apply clean_string to a pandas Series."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_string.py` around lines 89 -
91, Add type hints to clean_string_series: annotate the parameter as s:
pd.Series[Any] and the return type as -> pd.Series[Any] (import Any from typing
if not present). Keep the implementation the same (it maps strings and leaves
other values), and ensure pandas is referenced as pd so the annotation uses
pd.Series[Any]; update imports accordingly if needed.
models/br_tse_eleicoes/code/PLAN.md (1)

160-166: Consider adding language specifiers to fenced code blocks.

The column schema code blocks lack language specifiers (e.g., ```text or ```csv). Adding them improves rendering in some Markdown viewers and satisfies markdownlint (MD040).

This applies to all schema blocks from lines 160-310.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/PLAN.md` around lines 160 - 166, The fenced code
blocks that list column schemas (the blocks containing lines like "id_eleicao,
tipo_eleicao, data_eleicao, ..." and other schema lists) are missing language
specifiers; update each schema fenced code block in PLAN.md to include a
language tag (e.g., ```text or ```csv) so Markdown renderers and markdownlint
(MD040) recognize them, ensuring you add the specifier to all schema blocks in
that section.
models/br_tse_eleicoes/code/python/utils/clean_election_type.py (2)

13-14: Remove redundant empty string from the set.

The empty string "" at line 14 is unreachable because if not val: at line 9 already returns early for empty strings.

Suggested fix
     ordinaria_variants = {
-        "",
         "ordinaria",
         f"eleicoes ordinarias - {ano_s}",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_election_type.py` around lines
13 - 14, Remove the redundant empty string entry from the ordinaria_variants set
in clean_election_type (the set named ordinaria_variants) since the function
already returns early on empty values via the existing if not val: check; update
the set literal to exclude "" so the unreachable item is removed and no behavior
changes occur.

40-44: Add type hints for clean_election_type_series.

Missing type hints for parameter s and return type per coding guidelines.

Suggested fix
-def clean_election_type_series(s, ano: int):
+def clean_election_type_series(s: "pd.Series", ano: int) -> "pd.Series":
     """Apply clean_election_type to a pandas Series."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_election_type.py` around lines
40 - 44, Add type hints to clean_election_type_series: annotate the parameter s
as a pandas Series and the return type as a pandas Series (e.g., s: pd.Series
and -> pd.Series). Ensure the module imports pandas as pd or imports Series from
pandas so the annotation resolves; keep the existing use of
clean_election_type(…, ano) unchanged. Update the function signature to: def
clean_election_type_series(s: pd.Series, ano: int) -> pd.Series:.
models/br_tse_eleicoes/code/python/utils/clean_result.py (1)

17-23: Add missing type hints and docstring per coding guidelines.

clean_result lacks a docstring, and clean_result_series lacks type hints for both its parameter and return type.

Suggested fix
 def clean_result(val: str) -> str:
+    """Return the canonical result label for known variants, else unchanged."""
     return _MAP.get(val, val)
 
 
-def clean_result_series(s):
+def clean_result_series(s: "pd.Series") -> "pd.Series":
     """Apply clean_result to a pandas Series."""
     return s.map(lambda v: clean_result(v) if isinstance(v, str) else v)

Note: You'll need to add from __future__ import annotations at the top or use a string literal for the type hint to avoid importing pandas at module level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_result.py` around lines 17 -
23, Add a concise docstring to clean_result describing its purpose and
parameters/return, and annotate clean_result_series with parameter and return
types (e.g., s: "pd.Series" -> -> "pd.Series"); to avoid importing pandas at
module scope either add from __future__ import annotations at the top of the
module or use string-literal type hints, and ensure the docstring for
clean_result_series describes the input Series type and that it applies
clean_result to string elements.
models/br_tse_eleicoes/code/python/utils/clean_party.py (1)

43-58: Add missing docstring and type hints.

clean_party lacks a docstring, and clean_party_series lacks type hints for s and return type.

Suggested fix
 def clean_party(val: str, ano: int) -> str:
+    """Normalize party abbreviation applying year-specific then universal renames."""
     if not val:
         return val
     # Year-specific
     year_map = _YEAR_MAP.get(ano, {})
     if val in year_map:
         val = year_map[val]
     # Always
     if val in _ALWAYS_MAP:
         val = _ALWAYS_MAP[val]
     return val
 
 
-def clean_party_series(s, ano: int):
+def clean_party_series(s: "pd.Series", ano: int) -> "pd.Series":
     """Apply clean_party to a pandas Series."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_party.py` around lines 43 -
58, Add a short docstring to clean_party describing parameters (val: str, ano:
int) and return value (normalized party string), and update clean_party_series
to include type hints: change signature to def clean_party_series(s:
pandas.Series, ano: int) -> pandas.Series (or use pd.Series with an import
alias), ensure pandas is imported as pd if not already, and keep the
implementation returning s.map(lambda v: clean_party(v, ano) if isinstance(v,
str) else v); mention in the docstring that clean_party uses _YEAR_MAP and
_ALWAYS_MAP for normalization.
models/br_tse_eleicoes/code/python/utils/clean_marital_status.py (1)

18-26: Add missing type hints and docstring per coding guidelines.

clean_marital_status lacks a docstring, and clean_marital_status_series lacks type hints.

Suggested fix
 def clean_marital_status(val: str) -> str:
+    """Return the canonical marital status label for known variants, else unchanged."""
     return _MAP.get(val, val)
 
 
-def clean_marital_status_series(s):
+def clean_marital_status_series(s: "pd.Series") -> "pd.Series":
     """Apply clean_marital_status to a pandas Series."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/clean_marital_status.py` around
lines 18 - 26, Add a concise docstring to clean_marital_status describing its
purpose, input, and return value, and add type hints plus a docstring to
clean_marital_status_series (e.g., annotate parameter s as pandas.Series and
return type as pandas.Series); ensure the module imports pandas as pd if needed
and the series function's docstring explains it maps clean_marital_status over
string values while leaving non-strings unchanged (refer to clean_marital_status
and clean_marital_status_series).
models/br_tse_eleicoes/code/python/sub/vacancies.py (1)

34-35: Add Google-style docstrings and an explicit -> None on the entrypoint.

build_all() still omits a return annotation, and both function docstrings are the short one-line form. Tightening these signatures now will keep the generated builders consistent.

As per coding guidelines, **/*.py: Add type hints and docstrings for Python functions following Google Style.

Also applies to: 123-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/vacancies.py` around lines 34 - 35,
Update the two functions to use Google-style docstrings and add an explicit
return annotation for the entrypoint: change build_all() to build_all() -> None
and replace the short one-line docstrings in both build_vagas(ano: int) ->
pd.DataFrame and build_all() -> None with full Google-style docstrings that
include Args (for ano), Returns (for build_vagas), and a short Description; keep
existing type hints (pd.DataFrame and int) and ensure the wording matches the
project's docstring conventions.
models/br_tse_eleicoes/code/python/sub/voter_profile_polling_place.py (1)

14-15: Add explicit -> None and Google-style docstrings to the builders.

build_all() is still unannotated, and the current one-line docstrings do not match the repo’s documented Python style. This module is part of the public ETL surface, so it is worth keeping it aligned.

As per coding guidelines, **/*.py: Add type hints and docstrings for Python functions following Google Style.

Also applies to: 125-126

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voter_profile_polling_place.py` around
lines 14 - 15, Update the builder functions to include explicit return type
annotations and Google-style docstrings: add "-> None" to
build_perfil_local_votacao and to build_all (and any other builder functions in
this module), and replace the current one-line triple-quoted strings with full
Google-style docstrings describing Args, Returns (use "None" for these
builders), and a short description; ensure function signature type hints remain
(e.g., ano: int) and that the docstrings follow the repo's Python style
guidelines.
models/br_tse_eleicoes/code/python/sub/voter_profile_mun_zone.py (1)

12-13: Add explicit -> None and Google-style docstrings to the builders.

build_all() is still unannotated, and both docstrings are the short one-line form. This module is likely to become a template for other builders, so it is worth aligning it with the repo’s Python conventions now.

As per coding guidelines, **/*.py: Add type hints and docstrings for Python functions following Google Style.

Also applies to: 136-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voter_profile_mun_zone.py` around
lines 12 - 13, Update the builder functions to include explicit return type
hints and Google-style docstrings: change the signature of build_perfil_mun_zona
to include an explicit return type (-> pd.DataFrame if it returns a DataFrame,
or -> None if it only performs side effects) and convert its one-line docstring
to a full Google-style docstring (Args, Returns, Raises as applicable); do the
same for build_all (add explicit -> None if it returns nothing and a
Google-style docstring). Locate the functions by name (build_perfil_mun_zona,
build_all) and ensure imports/types remain correct when adding annotations and
expanding the docstrings.
models/br_tse_eleicoes/code/python/build.py (1)

56-56: Type the CLI callables and document the entrypoints.

_run_step() leaves func untyped, and the step runners plus main() still omit explicit -> None and Google-style docstrings. This is the main orchestration surface, so making the signatures self-describing is worth it.

As per coding guidelines, **/*.py: Add type hints and docstrings for Python functions following Google Style.

Also applies to: 66-66, 72-72, 78-79, 98-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/build.py` at line 56, Add explicit typing
and Google-style docstrings for the CLI entrypoints: annotate _run_step(name:
str, func) to accept func: Callable[[], None] (or Callable[..., None] if it may
take args) and return -> None, and do the same for each step runner function and
main() (e.g., add -> None). Import Callable from typing and update signatures
for the functions referenced in the review (the step runner functions and main),
then add brief Google-style docstrings to each function describing purpose,
Args, and Returns. Ensure the annotations match actual usage (adjust
Callable[...] if the function takes args) and keep docstrings concise and
informative.
models/br_tse_eleicoes/code/python/sub/results_section.py (1)

208-226: Collapse legenda to one row per merge key before joining.

nominais is already grouped, but legenda is merged raw. Grouping legenda too makes the outer join one-row-per-key and avoids accidental fan-out if the raw data contains duplicate legend rows.

♻️ Suggested refactor
         legenda = legenda.rename(
             columns={
                 "numero_votavel": "numero_partido",
                 "votos": "votos_legenda",
             }
         )
-        leg_cols = [c for c in group_cols if c in legenda.columns] + [
-            "votos_legenda"
-        ]
-        legenda = legenda[leg_cols]
+        leg_group = [c for c in group_cols if c in legenda.columns]
+        legenda = legenda[leg_group + ["votos_legenda"]]
+        legenda = legenda.groupby(
+            leg_group, as_index=False, dropna=False
+        )["votos_legenda"].sum()
 
         # merge nominais + legenda — use all shared group columns as keys
         merge_keys = [c for c in available_group if c in legenda.columns]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/results_section.py` around lines 208 -
226, legenda is not aggregated before merging with nom_agg, which can cause
fan-out when duplicate legend rows exist; collapse legenda to one row per merge
key by grouping on the same keys used for the merge (use merge_keys or the
intersection of available_group/group_cols present in legenda) and summing or
aggregating "votos_legenda" (and preserving any other needed columns) before
performing partido = nom_agg.merge(legenda, on=merge_keys, how="outer"); ensure
the grouped dataframe columns match leg_cols so the outer join remains
one-row-per-key.
models/br_tse_eleicoes/code/python/normalization_partition.py (1)

488-938: Add explicit -> None annotations on the partition/orchestration helpers.

These functions are pure orchestration and already have short docstrings, but the new _partition_* helpers and build_all are still unannotated. As per coding guidelines, **/*.py: Add type hints and docstrings for Python functions following Google Style.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/normalization_partition.py` around lines
488 - 938, The partition/orchestration helpers lack explicit return type
annotations; update each helper (e.g., _partition_candidatos,
_partition_partidos, _partition_simple, _partition_results_mun_zone,
_partition_results_section, _partition_bens, _partition_finance,
_partition_vagas, and build_all) to include a -> None return annotation on their
signatures and keep their existing docstrings intact; ensure the function
signatures are the only change (no logic modifications) so type checkers
recognize these as returning None.
models/br_tse_eleicoes/code/python/aggregation.py (1)

16-36: Finish typing the aggregation helpers.

The new aggregation entrypoints all return nothing, but they are still missing explicit -> None annotations, and _read_parquet is undocumented. As per coding guidelines, **/*.py: Add type hints and docstrings for Python functions following Google Style.

Also applies to: 56-506

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/aggregation.py` around lines 16 - 36, Add
explicit return type annotations and docstrings: update all aggregation
entrypoint functions that currently return nothing to have an explicit "-> None"
return type and add a one-line Google-style docstring describing their purpose
and side effects; specifically, add a docstring and a return type annotation "->
pd.DataFrame" for the helper _read_parquet(name: str, ano: int) to match
_read_partitioned_csv (which already has a docstring), and ensure all helper
functions include concise Google-style docstrings and precise type hints (e.g.,
_read_parquet: -> pd.DataFrame; aggregation entrypoints: -> None) so they comply
with the project's Python typing and documentation guidelines.
models/br_tse_eleicoes/code/python/sub/campaign_finance.py (1)

163-1983: Document the year-specific loaders and annotate the entrypoint.

The _build_receitas_* and _build_despesas_* helpers are where the raw layouts diverge, but most of them still have no function docs, and build_all has no explicit -> None. As per coding guidelines, **/*.py: Add type hints and docstrings for Python functions following Google Style.

Also applies to: 2005-2033

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/campaign_finance.py` around lines 163
- 1983, The review asks to add missing docstrings and explicit type annotations:
add Google-style docstrings to each year-specific loader (e.g.,
_build_receitas_2002, _build_receitas_2004, ..., _build_receitas_2018_plus and
_build_despesas_2002, _build_despesas_2004, ..., _build_despesas_2018_plus)
describing purpose, parameters (none) and return type, and ensure all functions
have explicit return type hints (they already return pd.DataFrame) and any
helper signatures include types where missing; also annotate the public
entrypoints build_receitas and build_despesas with -> pd.DataFrame and
add/annotate build_all (the module entrypoint) with -> None plus a docstring
explaining behavior; keep docstrings concise Google-style and update any untyped
parameters/returns across these functions to satisfy the guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d77f0f4-9174-4cea-b2a9-5c8e56670e45

📥 Commits

Reviewing files that changed from the base of the PR and between 8e57cb9 and 6982d24.

📒 Files selected for processing (30)
  • models/br_tse_eleicoes/code/PLAN.md
  • models/br_tse_eleicoes/code/[dbt]br_tse_eleicoes.ipynb
  • models/br_tse_eleicoes/code/python/aggregation.py
  • models/br_tse_eleicoes/code/python/build.py
  • models/br_tse_eleicoes/code/python/config.py
  • models/br_tse_eleicoes/code/python/normalization_partition.py
  • models/br_tse_eleicoes/code/python/sub/__init__.py
  • models/br_tse_eleicoes/code/python/sub/campaign_finance.py
  • models/br_tse_eleicoes/code/python/sub/candidates.py
  • models/br_tse_eleicoes/code/python/sub/parties.py
  • models/br_tse_eleicoes/code/python/sub/results_mun_zone.py
  • models/br_tse_eleicoes/code/python/sub/results_section.py
  • models/br_tse_eleicoes/code/python/sub/results_state.py
  • models/br_tse_eleicoes/code/python/sub/vacancies.py
  • models/br_tse_eleicoes/code/python/sub/voter_profile_mun_zone.py
  • models/br_tse_eleicoes/code/python/sub/voter_profile_polling_place.py
  • models/br_tse_eleicoes/code/python/sub/voter_profile_section.py
  • models/br_tse_eleicoes/code/python/sub/voting_details_mun_zone.py
  • models/br_tse_eleicoes/code/python/sub/voting_details_section.py
  • models/br_tse_eleicoes/code/python/sub/voting_details_state.py
  • models/br_tse_eleicoes/code/python/utils/__init__.py
  • models/br_tse_eleicoes/code/python/utils/clean_education.py
  • models/br_tse_eleicoes/code/python/utils/clean_election_type.py
  • models/br_tse_eleicoes/code/python/utils/clean_marital_status.py
  • models/br_tse_eleicoes/code/python/utils/clean_party.py
  • models/br_tse_eleicoes/code/python/utils/clean_result.py
  • models/br_tse_eleicoes/code/python/utils/clean_string.py
  • models/br_tse_eleicoes/code/python/utils/fix_candidate.py
  • models/br_tse_eleicoes/code/python/utils/helpers.py
  • models/br_tse_eleicoes/code/python/validate.py
💤 Files with no reviewable changes (1)
  • models/br_tse_eleicoes/code/[dbt]br_tse_eleicoes.ipynb

Comment thread models/br_tse_eleicoes/code/python/sub/campaign_finance.py
Comment thread models/br_tse_eleicoes/code/python/sub/results_mun_zone.py
Comment on lines +19 to +29
2002: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2004: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2006: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2008: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2010: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2012: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2014: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2016: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2018: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2020: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2022: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle BR in the Brazil-only filter.

The mapping switches to "BR" for national files from 2002 onward. Because this branch only checks "BRASIL", those years bypass the presidential/DF restriction and duplicate rows from the per-UF inputs.

🔧 Suggested fix
-        if uf == "BRASIL":
+        if uf in {"BR", "BRASIL"}:
             df = df[(df["cargo"] == "PRESIDENTE") | (df["sigla_uf"] == "DF")]

Also applies to: 248-250

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voting_details_mun_zone.py` around
lines 19 - 29, The Brazil-only filter incorrectly only checks for the string
"BRASIL" while the year->UF mapping includes the "BR" code for national files
(e.g., entries in the mapping for years 2002..2022), causing those rows to
bypass the presidential/DF restriction and create duplicates; update the filter
logic used in the relevant function(s) that process per-UF vs national files
(the branch that checks "BRASIL") to also detect the "BR" UF code (and any
equivalent national token) and apply the same presidential/DF restriction for
those records; ensure the same fix is applied to the analogous check around
lines 248-250.

Comment on lines +18 to +28
2002: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2004: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2006: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2008: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2010: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2012: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2014: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2016: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2018: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2020: ["AC", "AL", "AM", "AP", "BA", "CE", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],
2022: ["AC", "AL", "AM", "AP", "BA", "BR", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Treat BR as a national file too.

The year map switches to "BR" from 2002 onward, so this branch never runs for those years. Those national files then keep all cargos instead of only the presidential/DF slice and duplicate rows already loaded from the UF files.

🔧 Suggested fix
-        if uf == "BRASIL":
+        if uf in {"BR", "BRASIL"}:
             df = df[(df["cargo"] == "PRESIDENTE") | (df["sigla_uf"] == "DF")]

Also applies to: 123-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voting_details_section.py` around
lines 18 - 28, The code that decides whether a file is national currently misses
files labeled "BR" so the national-branch never runs for years where YEAR_UF_MAP
includes "BR"; update the national-file detection to treat uf == "BR" as a
national file (in the function/method that branches on UF, e.g.,
process_voting_details_section / _is_national_file or the code path that slices
to presidential/DF), and ensure the branch for national files applies the
presidential/DF slice and avoids reloading UF rows; also apply the same fix to
the other occurrence referenced around the block at lines 123-125.

Comment on lines +74 to +78
if df is None:
print(
f" WARNING: no file found for detalhes_votacao_uf {ano} {uf}"
)
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fail the year build when an expected raw file is missing.

continue here writes a partial parquet instead of surfacing incomplete raw storage. For a parity ETL, that is silent data loss.

🔧 Suggested fix
-        if df is None:
-            print(
-                f"    WARNING: no file found for detalhes_votacao_uf {ano} {uf}"
-            )
-            continue
+        if df is None:
+            msg = (
+                f"No file found for detalhes_votacao_uf "
+                f"{ano} {uf}"
+            )
+            raise FileNotFoundError(msg)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if df is None:
print(
f" WARNING: no file found for detalhes_votacao_uf {ano} {uf}"
)
continue
if df is None:
msg = (
f"No file found for detalhes_votacao_uf "
f"{ano} {uf}"
)
raise FileNotFoundError(msg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/sub/voting_details_state.py` around lines
74 - 78, The current block in voting_details_state.py that checks "if df is
None" (for detalhes_votacao_uf with ano and uf) must not silently continue
because that writes partial parquet; instead, stop the build by replacing the
print+continue with a hard failure: raise a descriptive exception (e.g.,
RuntimeError including ano, uf and the missing-file context) or call sys.exit(1)
so the year build fails immediately and surfaces the missing raw file error;
remove the continue and ensure any outer code will not write incomplete output
when df is missing.

Comment on lines +262 to +269
for keys, group in df.groupby(partition_cols, sort=True):
if isinstance(keys, str):
keys = (keys,)

parts = "/".join(
f"{col}={val}"
for col, val in zip(partition_cols, keys, strict=True)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n models/br_tse_eleicoes/code/python/utils/helpers.py | sed -n '250,280p'

Repository: basedosdados/pipelines

Length of output: 1081


🏁 Script executed:

cat -n models/br_tse_eleicoes/code/python/utils/helpers.py | sed -n '240,290p'

Repository: basedosdados/pipelines

Length of output: 1360


🏁 Script executed:

python3 << 'EOF'
import pandas as pd
import numpy as np

# Test pandas groupby with single column (non-string type)
df = pd.DataFrame({
    'ano': [2020, 2021, 2020, 2021],
    'value': [1, 2, 3, 4]
})

print("Test 1: Single numeric column groupby")
for keys, group in df.groupby(['ano'], sort=True):
    print(f"Keys type: {type(keys)}, Keys value: {keys}, Is tuple: {isinstance(keys, tuple)}")

print("\nTest 2: Single string column groupby")
df2 = pd.DataFrame({
    'estado': ['SP', 'MG', 'SP', 'MG'],
    'value': [1, 2, 3, 4]
})

for keys, group in df2.groupby(['estado'], sort=True):
    print(f"Keys type: {type(keys)}, Keys value: {keys}, Is tuple: {isinstance(keys, tuple)}")

print("\nTest 3: Multiple column groupby")
df3 = pd.DataFrame({
    'ano': [2020, 2021, 2020, 2021],
    'estado': ['SP', 'MG', 'SP', 'MG'],
    'value': [1, 2, 3, 4]
})

for keys, group in df3.groupby(['ano', 'estado'], sort=True):
    print(f"Keys type: {type(keys)}, Keys value: {keys}, Is tuple: {isinstance(keys, tuple)}")
EOF

Repository: basedosdados/pipelines

Length of output: 184


🏁 Script executed:

rg -l "save_partitioned" --type py

Repository: basedosdados/pipelines

Length of output: 179


🏁 Script executed:

rg "save_partitioned\(" --type py -B 2 -A 5

Repository: basedosdados/pipelines

Length of output: 9469


Single-column numeric partitions break save_partitioned().

For groupby(partition_cols) with one partition column, pandas returns a scalar key. The current normalization only wraps strings, so cases like partition_cols=["ano"] (where "ano" is numeric) reach zip(..., strict=True) with a non-iterable scalar, causing a TypeError.

🔧 Suggested fix
-        if isinstance(keys, str):
+        if not isinstance(keys, tuple):
             keys = (keys,)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for keys, group in df.groupby(partition_cols, sort=True):
if isinstance(keys, str):
keys = (keys,)
parts = "/".join(
f"{col}={val}"
for col, val in zip(partition_cols, keys, strict=True)
)
for keys, group in df.groupby(partition_cols, sort=True):
if not isinstance(keys, tuple):
keys = (keys,)
parts = "/".join(
f"{col}={val}"
for col, val in zip(partition_cols, keys, strict=True)
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/br_tse_eleicoes/code/python/utils/helpers.py` around lines 262 - 269,
The group key normalization only handles string keys, so numeric or other scalar
keys from df.groupby(partition_cols) (e.g., partition_cols=["ano"]) remain
non-iterable and break zip(..., strict=True); update the normalization in the
save_partitioned helper (around the df.groupby loop handling keys and parts) to
wrap any non-tuple key into a tuple (e.g., if not isinstance(keys, tuple): keys
= (keys,)) so zip receives an iterable of values corresponding to
partition_cols.

@mergify

mergify Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

mergify Bot and others added 11 commits June 19, 2026 11:45
…ão Prefect 3

Track A — corrige o parsing posicional (vN) que dessincronizou quando o TSE
reinseriu colunas. Novo utils/layout.py (resolve_columns lê os layouts oficiais
do harness); read_raw_csv aceita family/ano. Builders convertidos para seleção
por nome oficial TSE: candidates, results_mun_zone, voting_details_mun_zone,
parties, vacancies, voter_profile_mun_zone, campaign_finance (despesas 2014).
Harness tier3: FAIL 62 -> 0.

Track B — 3 rollups municipio reescritos como ref()+GROUP BY in-warehouse
(detalhes recomputa as 4 proporcoes via safe_divide).

Track D — pipelines/br_tse_eleicoes/ (Prefect 3): 1 flow/deployment por tabela
+ orquestrador async br_tse_eleicoes__refresh (run_deployment + asyncio.gather
nas waves de DEPENDENCIES.md). So orquestracao; invoca os builders de
models/code/python.

Docs: decisoes_migracao.md, ARQUITETURA.md, DIAGNOSIS.md regenerado.
… harness

Abandon the header-based parsing rewrite, dbt rollups and orphan Prefect 3
orchestration. Restore the original positional-column builders and the three
municipio dbt models to their aafb007 state so the existing code can be
bug-fixed in place, guided by the diagnostics harness (kept intact).
@mergify

mergify Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@mergify

mergify Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@rdahis

rdahis commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Posso ajudar com algo nesse PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

check-metadata [PR] Dispara validação de metadados entre BigQuery e API de produção data Nova base, conjunto ou tabela a ser adicionada à BD

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants