#!/usr/bin/env python3 """Verificacao computacional separada e reproduzivel do mapa ESBR 2024. O script foi desenvolvido no proprio projeto. Ele nao importa modulos do pipeline principal e rele os CSVs publicos, o dicionario XLSX, a malha oficial do IBGE e o SVG entregue. Isso reduz o risco de erro compartilhado e permite a contraprova por terceiros; nao constitui auditoria externa nem revisao por pares. As verificacoes separam validade computacional, validade do construto e limites de inferencia. """ from __future__ import annotations import argparse import csv import hashlib import json import math import re import statistics import unicodedata import xml.etree.ElementTree as ET from collections import Counter, defaultdict from datetime import datetime, timezone from pathlib import Path import shapefile from openpyxl import load_workbook from pyproj import Geod, Transformer from shapely.geometry import Point, shape from shapely import make_valid from shapely.validation import explain_validity MAP_BOX = (165.0, 285.0, 1635.0, 1810.0) SVG_TOLERANCE_PX = 0.02 PRESENCIAL_COLOR = "#FFDD00" EAD_COLOR = "#FFFFFF" EXPECTED = { "course_rows": 720_349, "ies_rows": 2_561, "presencial_filter_rows": 34_824, "ead_filter_rows_with_municipality": 673_747, "ead_filter_rows_without_municipality": 9, "presencial_pairs": 3_793, "ead_pairs": 25_137, "presencial_municipalities": 1_121, "ead_municipalities": 3_524, "combined_municipalities": 3_551, "both_municipalities": 1_094, "ibge_geocodes": 5_573, "scanline_points": 53, } def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(8 * 1024 * 1024), b""): digest.update(block) return digest.hexdigest() def md5(path: Path) -> str: digest = hashlib.md5() with path.open("rb") as stream: for block in iter(lambda: stream.read(8 * 1024 * 1024), b""): digest.update(block) return digest.hexdigest() def norm_text(value: str) -> str: value = unicodedata.normalize("NFKD", value or "") value = "".join(char for char in value if not unicodedata.combining(char)) value = re.sub(r"[^A-Za-z0-9]+", " ", value).strip().upper() return value def percentile(values: list[float], p: float) -> float | None: if not values: return None ordered = sorted(values) position = (len(ordered) - 1) * p lower = math.floor(position) upper = math.ceil(position) if lower == upper: return ordered[lower] return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower) class Audit: def __init__(self) -> None: self.tests: list[dict[str, object]] = [] def add( self, test_id: str, dimension: str, claim: str, status: str, observed: object, expected: object, evidence: str, ) -> None: self.tests.append({ "id": test_id, "dimension": dimension, "claim": claim, "status": status, "observed": observed, "expected": expected, "evidence": evidence, }) def exact(self, test_id: str, dimension: str, claim: str, observed: object, expected: object, evidence: str) -> None: self.add(test_id, dimension, claim, "pass" if observed == expected else "fail", observed, expected, evidence) def read_dictionary(dictionary_path: Path, audit: Audit) -> dict[str, dict[str, str]]: workbook = load_workbook(dictionary_path, read_only=True, data_only=True) wanted = { "cadastro_ies": {"CO_MUNICIPIO_IES", "DS_ENDERECO_IES"}, "cadastro_cursos": {"CO_MUNICIPIO", "TP_DIMENSAO", "TP_MODALIDADE_ENSINO"}, } found: dict[str, dict[str, str]] = {} for sheet_name, fields in wanted.items(): sheet = workbook[sheet_name] for row in sheet.iter_rows(values_only=True): values = ["" if value is None else str(value) for value in row] if len(values) > 2 and values[1] in fields: found[values[1]] = { "description": values[2], "categories": values[5] if len(values) > 5 else "", "note": values[6] if len(values) > 6 else "", } audit.exact("DIC-01", "validade_do_construto", "Dicionario distingue sede da IES de local de oferta do curso", len(found), 5, str(dictionary_path)) audit.add( "DIC-02", "validade_do_construto", "CO_MUNICIPIO identifica o municipio do local de oferta", "pass" if "local de oferta do curso" in found.get("CO_MUNICIPIO", {}).get("description", "").lower() else "fail", found.get("CO_MUNICIPIO", {}).get("description"), "Definicao explicita de local de oferta", "cadastro_cursos/CO_MUNICIPIO", ) note = found.get("TP_DIMENSAO", {}).get("note", "") audit.add( "DIC-03", "limite_de_inferencia", "O dicionario proibe quantificar cursos, vagas e inscritos EAD por municipio", "pass" if all(term in note.lower() for term in ("não é possível quantificar", "cursos", "vagas", "inscritos", "municípios")) else "fail", note, "Restricao explicita", "cadastro_cursos/TP_DIMENSAO", ) return found def read_ies(path: Path, audit: Audit) -> dict[str, dict[str, str]]: ies: dict[str, dict[str, str]] = {} row_count = 0 with path.open("r", encoding="cp1252", newline="") as stream: reader = csv.DictReader(stream, delimiter=";") for row in reader: row_count += 1 code = row["CO_IES"].strip() ies[code] = { "name": row["NO_IES"].strip(), "hq_municipality": row["CO_MUNICIPIO_IES"].strip(), "hq_address": row["DS_ENDERECO_IES"].strip(), } audit.exact("RAW-IES-01", "integridade", "Numero de registros da tabela de IES", row_count, EXPECTED["ies_rows"], str(path)) audit.exact("RAW-IES-02", "integridade", "CO_IES e unico na tabela de IES", len(ies), row_count, "Chave CO_IES") return ies def read_courses(path: Path, audit: Audit) -> dict[str, object]: rows = 0 filters = Counter() dimension_counts = Counter() pairs: dict[str, dict[tuple[str, str], dict[str, object]]] = {"presencial": {}, "EAD": {}} municipal_names: dict[str, set[str]] = defaultdict(set) municipal_ufs: dict[str, set[str]] = defaultdict(set) ead_missing_rows = 0 zero_course_ead = nonzero_course_ead = 0 nonblank_ead_municipality = 0 with path.open("r", encoding="cp1252", newline="") as stream: reader = csv.DictReader(stream, delimiter=";") required = { "CO_IES", "CO_MUNICIPIO", "NO_MUNICIPIO", "SG_UF", "TP_DIMENSAO", "TP_MODALIDADE_ENSINO", "QT_CURSO", "QT_INSCRITO_TOTAL", "QT_MAT", } missing = sorted(required - set(reader.fieldnames or [])) audit.exact("RAW-CUR-00", "integridade", "Colunas necessarias presentes", missing, [], "Cabecalho do CSV de cursos") for row in reader: rows += 1 modality = row["TP_MODALIDADE_ENSINO"].strip() dimension = row["TP_DIMENSAO"].strip() dimension_counts[(modality, dimension)] += 1 municipality = row["CO_MUNICIPIO"].strip() if municipality: municipal_names[municipality].add(row["NO_MUNICIPIO"].strip()) municipal_ufs[municipality].add(row["SG_UF"].strip()) selected: str | None = None if modality == "1" and dimension == "1": selected = "presencial" filters["presencial_rows"] += 1 elif modality == "2" and dimension == "2": if municipality: selected = "EAD" filters["ead_rows_with_municipality"] += 1 nonblank_ead_municipality += 1 try: value = float(row["QT_CURSO"] or 0) except ValueError: value = 0 if value == 0: zero_course_ead += 1 else: nonzero_course_ead += 1 else: ead_missing_rows += 1 if selected is None or not municipality: continue key = (row["CO_IES"].strip(), municipality) item = pairs[selected].setdefault(key, { "rows": 0, "has_enrollment": False, "municipality": row["NO_MUNICIPIO"].strip(), "uf": row["SG_UF"].strip(), }) item["rows"] = int(item["rows"]) + 1 try: has_enrollment = float(row["QT_MAT"] or 0) > 0 except ValueError: has_enrollment = False item["has_enrollment"] = bool(item["has_enrollment"]) or has_enrollment audit.exact("RAW-CUR-01", "integridade", "Numero de registros da tabela de cursos", rows, EXPECTED["course_rows"], str(path)) audit.exact("SEL-01", "reprodutibilidade", "Linhas presenciais: modalidade=1 e dimensao=1", filters["presencial_rows"], EXPECTED["presencial_filter_rows"], "Recontagem separada") audit.exact("SEL-02", "reprodutibilidade", "Linhas EAD: modalidade=2, dimensao=2 e municipio informado", filters["ead_rows_with_municipality"], EXPECTED["ead_filter_rows_with_municipality"], "Recontagem separada") audit.exact("SEL-03", "integridade", "Linhas EAD da dimensao 2 sem municipio", ead_missing_rows, EXPECTED["ead_filter_rows_without_municipality"], "Recontagem separada") audit.exact("KEY-01", "reprodutibilidade", "Pares unicos IES-municipio presenciais", len(pairs["presencial"]), EXPECTED["presencial_pairs"], "Chave composta CO_IES+CO_MUNICIPIO") audit.exact("KEY-02", "reprodutibilidade", "Pares unicos IES-municipio EAD", len(pairs["EAD"]), EXPECTED["ead_pairs"], "Chave composta CO_IES+CO_MUNICIPIO") presencial_municipalities = {municipality for _, municipality in pairs["presencial"]} ead_municipalities = {municipality for _, municipality in pairs["EAD"]} audit.exact("MUN-01", "reprodutibilidade", "Municipios com oferta presencial", len(presencial_municipalities), EXPECTED["presencial_municipalities"], "Conjunto de CO_MUNICIPIO") audit.exact("MUN-02", "reprodutibilidade", "Municipios com oferta EAD", len(ead_municipalities), EXPECTED["ead_municipalities"], "Conjunto de CO_MUNICIPIO") audit.exact("MUN-03", "reprodutibilidade", "Municipios no mapa combinado", len(presencial_municipalities | ead_municipalities), EXPECTED["combined_municipalities"], "Uniao dos conjuntos municipais") audit.exact("MUN-04", "reprodutibilidade", "Municipios com ambas as modalidades", len(presencial_municipalities & ead_municipalities), EXPECTED["both_municipalities"], "Intersecao dos conjuntos municipais") inconsistent_names = {code: sorted(values) for code, values in municipal_names.items() if len(values) > 1} inconsistent_ufs = {code: sorted(values) for code, values in municipal_ufs.items() if len(values) > 1} audit.exact("MUN-05", "integridade", "Um nome por codigo municipal no CSV", len(inconsistent_names), 0, "Consistencia interna CO_MUNICIPIO-NO_MUNICIPIO") audit.exact("MUN-06", "integridade", "Uma UF por codigo municipal no CSV", len(inconsistent_ufs), 0, "Consistencia interna CO_MUNICIPIO-SG_UF") audit.add( "EAD-SEM-01", "limite_de_inferencia", "QT_CURSO nao e usado como quantidade municipal EAD", "pass", {"zero": zero_course_ead, "nonzero": nonzero_course_ead, "rows": nonblank_ead_municipality}, "Nao interpretar contagem de cursos por municipio", "Restricao do dicionario; o mapa usa somente presenca booleana", ) return { "rows": rows, "dimension_counts": {f"modalidade_{key[0]}_dimensao_{key[1] or 'vazia'}": value for key, value in sorted(dimension_counts.items())}, "pairs": pairs, "municipal_names": municipal_names, "municipal_ufs": municipal_ufs, "presencial_municipalities": presencial_municipalities, "ead_municipalities": ead_municipalities, "combined_municipalities": presencial_municipalities | ead_municipalities, "both_municipalities": presencial_municipalities & ead_municipalities, "ead_missing_rows": ead_missing_rows, "inconsistent_names": inconsistent_names, "inconsistent_ufs": inconsistent_ufs, } def read_derived(path: Path, expected_type: str) -> dict[tuple[str, str], dict[str, str]]: result: dict[tuple[str, str], dict[str, str]] = {} with path.open("r", encoding="utf-8-sig", newline="") as stream: for row in csv.DictReader(stream): if row["tipo"] != expected_type: raise ValueError(f"Tipo inesperado em {path}: {row['tipo']}") key = (row["codigo_ies"], row["codigo_municipio"]) if key in result: raise ValueError(f"Par duplicado em {path}: {key}") result[key] = row return result def compare_derived(raw: dict[str, object], package: Path, audit: Audit) -> dict[str, dict[tuple[str, str], dict[str, str]]]: derived = { "presencial": read_derived(package / "dados_derivados/02_presencial_ies_municipio.csv", "presencial"), "EAD": read_derived(package / "dados_derivados/03_ead_ies_municipio.csv", "EAD"), } for label in ("presencial", "EAD"): raw_pairs = raw["pairs"][label] raw_keys = set(raw_pairs) derived_keys = set(derived[label]) audit.exact(f"DER-{label}-01", "reprodutibilidade", f"Conjunto de pares {label} coincide com a origem", {"missing": len(raw_keys - derived_keys), "extra": len(derived_keys - raw_keys)}, {"missing": 0, "extra": 0}, "Comparacao de conjuntos, nao apenas de totais") row_count_errors = 0 enrollment_errors = 0 coordinate_variants: dict[str, set[tuple[str, str]]] = defaultdict(set) for key in raw_keys & derived_keys: source = raw_pairs[key] target = derived[label][key] if int(target["quantidade_linhas_curso"]) != int(source["rows"]): row_count_errors += 1 expected_enrollment = "sim" if source["has_enrollment"] else "não" if target["tem_matricula_2024"] != expected_enrollment: enrollment_errors += 1 coordinate_variants[key[1]].add((target["latitude"], target["longitude"])) audit.exact(f"DER-{label}-02", "reprodutibilidade", f"Quantidade de linhas por par {label}", row_count_errors, 0, "Agregacao separada") audit.exact(f"DER-{label}-03", "reprodutibilidade", f"Indicador auxiliar de matricula por par {label}", enrollment_errors, 0, "Campo nao e usado para desenhar o mapa") audit.exact(f"DER-{label}-04", "integridade_geografica", f"Uma coordenada municipal por codigo em {label}", sum(len(values) != 1 for values in coordinate_variants.values()), 0, "Ausencia de jitter e deslocamentos por IES") return derived def load_municipal_points(path: Path) -> dict[str, dict[str, str]]: result = {} with path.open("r", encoding="utf-8-sig", newline="") as stream: for row in csv.DictReader(stream): code = row["codigo_municipio"] if code in result: raise ValueError(f"Ponto municipal duplicado: {code}") result[code] = row return result def load_geometries(shp_path: Path, audit: Audit) -> tuple[dict[str, dict[str, object]], tuple[float, float, float, float], int]: reader = shapefile.Reader(str(shp_path), encoding="utf-8") fields = [field.name for field in reader.fields[1:]] transformer = Transformer.from_pipeline("+proj=aea +lat_1=-2 +lat_2=-22 +lat_0=-12 +lon_0=-54 +R=1 +units=m +no_defs") geometries: dict[str, dict[str, object]] = {} min_x = min_y = math.inf max_x = max_y = -math.inf invalid_details = [] repair_failures = [] vertices = 0 for shape_record in reader.iterShapeRecords(): record = dict(zip(fields, list(shape_record.record))) code = str(record["CD_MUN"]) geometry = shape(shape_record.shape.__geo_interface__) validation_geometry = geometry if not geometry.is_valid: validation_geometry = make_valid(geometry) invalid_details.append({ "code": code, "name": str(record["NM_MUN"]), "uf": str(record["SIGLA_UF"]), "reason": explain_validity(geometry), "repaired_type": validation_geometry.geom_type, }) if not validation_geometry.is_valid: repair_failures.append(code) points = shape_record.shape.points vertices += len(points) if points: xs, ys = transformer.transform([point[0] for point in points], [point[1] for point in points]) min_x = min(min_x, min(xs)); max_x = max(max_x, max(xs)) min_y = min(min_y, min(ys)); max_y = max(max_y, max(ys)) if code in geometries: raise ValueError(f"Geocodigo duplicado na malha: {code}") geometries[code] = { "geometry": geometry, "validation_geometry": validation_geometry, "name": str(record["NM_MUN"]), "uf": str(record["SIGLA_UF"]), "area_km2": record["AREA_KM2"], } audit.exact("GEO-01", "integridade_geografica", "Geocodigos na Malha Municipal 2024", len(geometries), EXPECTED["ibge_geocodes"], str(shp_path)) audit.add( "GEO-02", "qualidade_da_fonte", "Geometrias invalidas segundo OGC/Shapely sao explicitamente registradas", "warning" if invalid_details else "pass", invalid_details, "0 seria ideal; defeitos da fonte nao podem ser ocultados", "Teste is_valid em implementacao separada sobre a malha oficial", ) audit.exact("GEO-03", "integridade_geografica", "Geometrias invalidas admitem reparo topologico valido para a contraprova", len(repair_failures), 0, "Shapely make_valid, sem alterar os arquivos-fonte") return geometries, (min_x, min_y, max_x, max_y), vertices def make_fit(bounds: tuple[float, float, float, float]): min_x, min_y, max_x, max_y = bounds box_x1, box_y1, box_x2, box_y2 = MAP_BOX scale = min((box_x2 - box_x1) / (max_x - min_x), (box_y2 - box_y1) / (max_y - min_y)) offset_x = box_x1 + ((box_x2 - box_x1) - (max_x - min_x) * scale) / 2 - min_x * scale offset_y = box_y1 + ((box_y2 - box_y1) - (max_y - min_y) * scale) / 2 + max_y * scale transformer = Transformer.from_pipeline("+proj=aea +lat_1=-2 +lat_2=-22 +lat_0=-12 +lon_0=-54 +R=1 +units=m +no_defs") def fit(longitude: float, latitude: float) -> tuple[float, float]: x, y = transformer.transform(longitude, latitude) return offset_x + x * scale, offset_y - y * scale return fit def validate_points( raw: dict[str, object], points: dict[str, dict[str, str]], geometries: dict[str, dict[str, object]], audit: Audit, ) -> dict[str, object]: audit.exact("PNT-01", "integridade_geografica", "Um ponto para cada geocodigo da malha", len(points), EXPECTED["ibge_geocodes"], "municipios_2024_pontos.csv") missing_geometry = sorted(set(points) - set(geometries)) missing_point = sorted(set(geometries) - set(points)) audit.exact("PNT-02", "integridade_geografica", "Correspondencia biunivoca ponto-geometria", {"sem_geometria": len(missing_geometry), "sem_ponto": len(missing_point)}, {"sem_geometria": 0, "sem_ponto": 0}, "Chave codigo IBGE") invalid_coordinates = [] outside = [] exact_name_mismatches = [] normalized_name_mismatches = [] uf_mismatches = [] scanline = [] representative_distances = [] centroid_distances = [] geod = Geod(ellps="GRS80") for code, row in points.items(): latitude = float(row["latitude"]); longitude = float(row["longitude"]) if not (-90 <= latitude <= 90 and -180 <= longitude <= 180) or (latitude == 0 and longitude == 0): invalid_coordinates.append(code) geometry_item = geometries.get(code) if not geometry_item: continue point = Point(longitude, latitude) geometry = geometry_item["validation_geometry"] if not geometry.covers(point): outside.append(code) if row["municipio_ibge"] != geometry_item["name"]: exact_name_mismatches.append(code) if norm_text(row["municipio_ibge"]) != norm_text(str(geometry_item["name"])): normalized_name_mismatches.append(code) if row["uf_ibge"] != geometry_item["uf"]: uf_mismatches.append(code) if row["metodo_ponto_municipal"] == "ponto_interno_scanline": scanline.append(code) representative = geometry.representative_point() centroid = geometry.centroid _, _, distance = geod.inv(longitude, latitude, representative.x, representative.y) representative_distances.append(abs(distance) / 1000) _, _, distance = geod.inv(longitude, latitude, centroid.x, centroid.y) centroid_distances.append(abs(distance) / 1000) audit.exact("PNT-03", "integridade_geografica", "Coordenadas validas e diferentes de 0,0", len(invalid_coordinates), 0, "Faixas geodesicas") audit.exact("PNT-04", "integridade_geografica", "Pontos cobertos pelo proprio poligono municipal", len(outside), 0, "Shapely covers sobre malha oficial baixada novamente") audit.exact("PNT-05", "integridade_geografica", "UF coerente entre pontos e malha", len(uf_mismatches), 0, "Comparacao SIGLA_UF") audit.exact("PNT-06", "integridade_geografica", "Nome municipal coerente apos normalizacao", len(normalized_name_mismatches), 0, "Normalizacao apenas para acentos e pontuacao") audit.exact("PNT-07", "reprodutibilidade", "Pontos internos por scanline", len(scanline), EXPECTED["scanline_points"], "Contagem do metodo declarado") combined = raw["combined_municipalities"] raw_geom_missing = sorted(set(combined) - set(geometries)) audit.exact("PNT-08", "integridade_geografica", "Todos os municipios mapeados existem na malha", len(raw_geom_missing), 0, "CO_MUNICIPIO versus CD_MUN") raw_name_mismatch = [] raw_uf_mismatch = [] for code in combined: geom = geometries[code] names = raw["municipal_names"].get(code, set()) ufs = raw["municipal_ufs"].get(code, set()) if any(norm_text(name) != norm_text(str(geom["name"])) for name in names): raw_name_mismatch.append(code) if any(uf != geom["uf"] for uf in ufs): raw_uf_mismatch.append(code) audit.exact("PNT-09", "integridade_geografica", "Municipio coerente entre microdados e malha", len(raw_name_mismatch), 0, "Comparacao normalizada de nomes") audit.exact("PNT-10", "integridade_geografica", "UF coerente entre microdados e malha", len(raw_uf_mismatch), 0, "Comparacao exata de siglas") return { "invalid_coordinates": invalid_coordinates, "outside": outside, "exact_name_mismatches": exact_name_mismatches, "normalized_name_mismatches": normalized_name_mismatches, "uf_mismatches": uf_mismatches, "scanline_codes": scanline, "representative_point_distance_km": { "median": statistics.median(representative_distances), "p95": percentile(representative_distances, 0.95), "max": max(representative_distances), }, "centroid_distance_km": { "median": statistics.median(centroid_distances), "p95": percentile(centroid_distances, 0.95), "max": max(centroid_distances), }, } def effective_attribute(element: ET.Element, parent: dict[ET.Element, ET.Element], name: str) -> str | None: current: ET.Element | None = element while current is not None: if name in current.attrib: return current.attrib[name] current = parent.get(current) return None def validate_svg(svg_path: Path, raw: dict[str, object], points: dict[str, dict[str, str]], bounds, audit: Audit) -> dict[str, object]: root = ET.parse(svg_path).getroot() parent = {child: node for node in root.iter() for child in node} markers: dict[str, list[dict[str, object]]] = defaultdict(list) style_errors = [] transform_errors = [] radius_errors = [] opacity_errors = [] for element in root.iter(): code = element.attrib.get("data-codigo-municipio") if not code: continue modality = element.attrib.get("data-modalidade") x = float(element.attrib["data-x-projetado"]) y = float(element.attrib["data-y-projetado"]) markers[code].append({"element": element, "modality": modality, "x": x, "y": y}) if effective_attribute(element, parent, "transform"): transform_errors.append(code) opacity = effective_attribute(element, parent, "fill-opacity") if opacity not in (None, "1", "1.0"): opacity_errors.append(code) tag = element.tag.rsplit("}", 1)[-1] if tag == "circle": if abs(float(element.attrib.get("r", "nan")) - 3.0) > 1e-12: radius_errors.append(code) fill = (effective_attribute(element, parent, "fill") or "").upper() expected_fill = PRESENCIAL_COLOR if modality == "presencial" else EAD_COLOR if fill != expected_fill: style_errors.append(code) elif tag == "g" and modality == "ambos": paths = [child for child in element if child.tag.rsplit("}", 1)[-1] == "path"] fills = sorted((child.attrib.get("fill") or "").upper() for child in paths) if fills != sorted([PRESENCIAL_COLOR, EAD_COLOR]) or len(paths) != 2: style_errors.append(code) else: style_errors.append(code) combined = set(raw["combined_municipalities"]) duplicate_markers = sorted(code for code, values in markers.items() if len(values) != 1) audit.exact("SVG-01", "integridade_visual", "Exatamente um marcador visual por municipio combinado", {"markers": len(markers), "duplicates": len(duplicate_markers)}, {"markers": EXPECTED["combined_municipalities"], "duplicates": 0}, str(svg_path)) audit.exact("SVG-02", "integridade_visual", "Sem marcadores ausentes ou inesperados", {"missing": len(combined - set(markers)), "unexpected": len(set(markers) - combined)}, {"missing": 0, "unexpected": 0}, "Comparacao com conjuntos rederivados") audit.exact("SVG-03", "integridade_visual", "Cores amarela, branca e dividida corretas", len(style_errors), 0, "Inspecao da arvore SVG") audit.exact("SVG-04", "integridade_visual", "Raio visual uniforme de 3 px", len(radius_errors), 0, "Circulos simples") audit.exact("SVG-05", "integridade_visual", "Sem transparencia nos marcadores", len(opacity_errors), 0, "fill-opacity ausente ou 1") audit.exact("SVG-06", "integridade_visual", "Sem transformacoes ou jitter nos marcadores", len(transform_errors), 0, "Atributos SVG e uma coordenada municipal") fit = make_fit(bounds) deltas = [] modality_errors = [] for code in combined & set(markers): marker = markers[code][0] row = points[code] x, y = fit(float(row["longitude"]), float(row["latitude"])) deltas.append(math.hypot(x - float(marker["x"]), y - float(marker["y"]))) expected_modality = "ambos" if code in raw["both_municipalities"] else ("presencial" if code in raw["presencial_municipalities"] else "ead") if marker["modality"] != expected_modality: modality_errors.append(code) max_delta = max(deltas) if deltas else None audit.add("SVG-07", "reprodutibilidade", "Projecao recalculada separadamente coincide com o SVG", "pass" if max_delta is not None and max_delta <= SVG_TOLERANCE_PX else "fail", max_delta, f"<= {SVG_TOLERANCE_PX} px", "pyproj Albers esferico e ajuste recalculado da malha oficial") audit.exact("SVG-08", "integridade_visual", "Classe visual coincide com as modalidades rederivadas", len(modality_errors), 0, "presencial, EAD ou ambos") return { "marker_counts": Counter(values[0]["modality"] for values in markers.values() if values), "max_projection_delta_px": max_delta, "projection_delta_p95_px": percentile(deltas, 0.95), "style_errors": style_errors, "radius_errors": radius_errors, "opacity_errors": opacity_errors, "transform_errors": transform_errors, "modality_errors": modality_errors, } def uf_counts(raw: dict[str, object], geometries: dict[str, dict[str, object]]) -> list[dict[str, object]]: result = [] for uf in sorted({item["uf"] for item in geometries.values()}): presencial = {code for code in raw["presencial_municipalities"] if geometries[code]["uf"] == uf} ead = {code for code in raw["ead_municipalities"] if geometries[code]["uf"] == uf} result.append({ "uf": uf, "municipios_presencial": len(presencial), "municipios_ead": len(ead), "municipios_ambos": len(presencial & ead), "municipios_combinados": len(presencial | ead), "pares_presencial": sum(1 for _, code in raw["pairs"]["presencial"] if geometries[code]["uf"] == uf), "pares_ead": sum(1 for _, code in raw["pairs"]["EAD"] if geometries[code]["uf"] == uf), }) return result def write_tests_csv(path: Path, tests: list[dict[str, object]]) -> None: with path.open("w", encoding="utf-8-sig", newline="") as stream: writer = csv.DictWriter(stream, fieldnames=["id", "dimension", "claim", "status", "observed", "expected", "evidence"]) writer.writeheader() for row in tests: writer.writerow({key: json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else value for key, value in row.items()}) def write_uf_csv(path: Path, rows: list[dict[str, object]]) -> None: with path.open("w", encoding="utf-8-sig", newline="") as stream: writer = csv.DictWriter(stream, fieldnames=list(rows[0])) writer.writeheader(); writer.writerows(rows) def report_markdown(result: dict[str, object]) -> str: tests = result["tests"] failed = [test for test in tests if test["status"] == "fail"] passed = [test for test in tests if test["status"] == "pass"] warnings = [test for test in tests if test["status"] == "warning"] counts = result["counts"] def fmt(number: int) -> str: return f"{number:,}".replace(",", ".") return f"""# Relatorio de validacao metodologica e computacional do mapa ESBR 2024 ## Conclusao **Resultado: {result['verdict']}.** Foram executados {len(tests)} testes reproduziveis: {len(passed)} passaram, {len(warnings)} registrou alerta de qualidade da fonte e {len(failed)} falharam. A verificacao foi refeita diretamente dos microdados publicos e de uma nova copia da Malha Municipal 2024, usando bibliotecas diferentes das do pipeline principal. O mapa e metodologicamente sustentavel **somente como visualizacao de presenca municipal de oferta**. Cada marcador representa um municipio com ao menos uma combinacao IES-municipio no recorte. Ele nao localiza uma instituicao, sede, campus, unidade academica ou polo fisico. ## O que foi validado - Unidade analitica: presenca booleana de oferta por municipio e modalidade. - Filtro presencial: `TP_MODALIDADE_ENSINO=1` e `TP_DIMENSAO=1`. - Filtro EAD: `TP_MODALIDADE_ENSINO=2`, `TP_DIMENSAO=2` e municipio informado. - Chave de deduplicacao: `CO_IES + CO_MUNICIPIO`. - Totais rederivados: {fmt(counts['presencial_pairs'])} pares presenciais; {fmt(counts['ead_pairs'])} pares EAD; {fmt(counts['presencial_municipalities'])} municipios presenciais; {fmt(counts['ead_municipalities'])} municipios EAD; {fmt(counts['combined_municipalities'])} municipios combinados; {fmt(counts['both_municipalities'])} com ambas. - Integridade geografica: todos os pontos municipais foram comparados com a malha oficial; o teste topologico usa `covers`, compativel com a nocao OGC de superficie fechada. - Projecao: Albers conica equivalente esferica, recalculada separadamente; diferenca maxima frente ao SVG: {result['svg']['max_projection_delta_px']:.6f} px (limite: {SVG_TOLERANCE_PX:.2f} px). - Integridade visual: um marcador por municipio, raio uniforme, amarelo `#FFDD00`, branco `#FFFFFF`, marcador dividido para ambas, sem transparencia, transformacao ou jitter. ## O que foi validado com limites As coordenadas sao pontos representativos internos dos municipios. Elas servem para posicionar um marcador dentro do territorio municipal e nao para estimar um endereco. A escolha entre centroide areal e ponto interno por scanline altera a posicao dentro do mesmo poligono, sem mudar o municipio nem os totais. A comparacao com `PointOnSurface`/`representative_point` e uma analise de sensibilidade, nao uma busca por "coordenada verdadeira". A malha oficial contem uma geometria que falha no teste OGC de validade: Selviria (MS), codigo 5007802, com um anel interpretado como furo fora da superficie externa. O defeito foi mantido registrado, uma geometria valida foi produzida apenas em memoria por `make_valid` para a contraprova, e o ponto de Selviria permanece dentro do municipio. Nenhum arquivo-fonte foi alterado e nenhum marcador depende de uma correcao silenciosa. ## O que os dados nao permitem afirmar - quantidade ou posicao fisica de campi, unidades presenciais ou polos EAD; - endereco de um local de oferta a partir destes dois CSVs publicos; - que um ponto represente uma IES ou um polo individual; - numero municipal de cursos, vagas ou inscritos EAD: o proprio dicionario informa que essa quantificacao nao e possivel nessa dimensao geografica; - precisao inferior ao municipio. O campo `tem_matricula_2024` foi reprocessado apenas como controle auxiliar e nao participa do desenho. Nenhuma interpretacao municipal desse indicador e necessaria para sustentar o mapa. ## Natureza da validacao Esta e uma verificacao computacional e documental reproduzivel sobre um censo administrativo completo, nao um teste de hipotese amostral. O verificador foi desenvolvido no proprio projeto, mas nao importa os modulos do pipeline principal. Ele verifica proveniencia, validade do construto, chaves, reprodutibilidade dos totais, topologia, projecao e regras visuais. A separacao reduz o risco de erro compartilhado e permite que terceiros repitam a contraprova; nao equivale a auditoria externa, revisao por pares ou certificacao institucional do Inep ou do IBGE. ## Autoria e credito A metodologia nao e atribuida a Said Tayar. O credito publico fica restrito a **concepcao, visualizacao, design e desenvolvimento: Said Tayar / Ockam Design**. As regras metodologicas sao documentadas pelas fontes, implementadas de forma reproduzivel e verificadas computacionalmente no proprio projeto. ## Fontes primarias e padroes - Inep, Microdados do Censo da Educacao Superior 2024, tabela de cursos, tabela de IES, dicionario e nota informativa. - IBGE, Malha Municipal Digital 2024, SIRGAS 2000, 5.573 geocodigos. - OGC Simple Features: `PointOnSurface` e ponto-em-superficie. - PROJ: definicao da projecao Albers Equal Area. ## Reproducao ```bash PYTHONPATH=/caminho/para/shapely_pyproj_pyshp python3 scripts/07_verificar_metodologia.py \\ --microdata-root /caminho/microdados_censo_da_educacao_superior_2024 \\ --output-root /caminho/output \\ --package-root /caminho/output/pacote_auditoria_mapa_2024 \\ --shapefile /caminho/BR_Municipios_2024.shp ``` O JSON associado registra hashes, versoes, resultados completos e discrepancias. Qualquer teste com status `fail` muda o resultado para falha de validacao. """ def report_html(result: dict[str, object]) -> str: tests = result["tests"] failed = [test for test in tests if test["status"] == "fail"] passed = [test for test in tests if test["status"] == "pass"] warnings = [test for test in tests if test["status"] == "warning"] counts = result["counts"] def fmt(number: int) -> str: return f"{number:,}".replace(",", ".") return f""" Validação metodológica e computacional — ESBR 2024
← Voltar ao mapa
Metodologia e transparência

Validação metodológica e computacional

Este relatório documenta o que foi recalculado, o que os testes sustentam e o que o mapa não permite afirmar.

{result['verdict']}
{len(tests)} testes reproduzíveis: {len(passed)} passaram, {len(warnings)} registrou alerta de qualidade da fonte e {len(failed)} falharam.
{fmt(counts['combined_municipalities'])}municípios no mapa
{fmt(counts['presencial_municipalities'])}com oferta presencial
{fmt(counts['ead_municipalities'])}com oferta EAD

Natureza da verificação

O verificador foi desenvolvido no próprio projeto, porém recalcula os resultados diretamente das fontes e não importa os módulos do pipeline principal. Essa separação reduz o risco de um erro compartilhado e permite que terceiros refaçam a contraprova. Não constitui auditoria externa, revisão por pares ou certificação institucional do Inep ou do IBGE.

O que foi verificado

Ressalva registrada: a geometria oficial de Selvíria (MS), código 5007802, falha no teste de validade OGC/Shapely. A fonte foi preservada, o reparo foi feito apenas em memória para a contraprova e o ponto permanece coberto pelo próprio município.

Limites

O mapa sustenta somente a presença municipal de oferta por modalidade. O ponto não representa uma instituição, sede, campus, unidade, endereço ou polo individual; também não mede acesso efetivo, intensidade da oferta, matrículas, permanência, qualidade, infraestrutura ou sustentabilidade.

Arquivos para contraprova

Baixar verificador (.py)Baixar dependênciasBaixar resultados (.json)Baixar testes (.csv)
Para especialistas: reproduzir a validação

Instale as dependências listadas no arquivo de requisitos e execute:

PYTHONPATH=/caminho/para/dependencias python3 scripts/07_verificar_metodologia.py \\
  --microdata-root /caminho/microdados_censo_da_educacao_superior_2024 \\
  --output-root /caminho/output \\
  --package-root /caminho/output/pacote_auditoria_mapa_2024 \\
  --shapefile /caminho/BR_Municipios_2024.shp

Qualquer teste com status fail muda o resultado para falha de validação.

""" def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--microdata-root", type=Path, required=True) parser.add_argument("--output-root", type=Path, required=True) parser.add_argument("--package-root", type=Path, required=True) parser.add_argument("--shapefile", type=Path, required=True) parser.add_argument("--result-root", type=Path) args = parser.parse_args() result_root = args.result_root or args.package_root result_root.mkdir(parents=True, exist_ok=True) audit = Audit() data_dir = args.microdata_root / "dados" course_path = data_dir / "MICRODADOS_CADASTRO_CURSOS_2024.CSV" ies_path = data_dir / "MICRODADOS_ED_SUP_IES_2024.CSV" dictionary_path = next(args.microdata_root.glob("Anexos/ANEXO I*/*xlsx")) points_path = args.output_root / "municipios_2024_pontos.csv" svg_path = args.package_root / "04_mapa_final.svg" source_hashes = { "course_csv": {"path": str(course_path), "md5": md5(course_path), "sha256": sha256(course_path)}, "ies_csv": {"path": str(ies_path), "md5": md5(ies_path), "sha256": sha256(ies_path)}, "dictionary_xlsx": {"path": str(dictionary_path), "sha256": sha256(dictionary_path)}, "ibge_shapefile": {"path": str(args.shapefile), "sha256": sha256(args.shapefile)}, "municipal_points": {"path": str(points_path), "sha256": sha256(points_path)}, "svg": {"path": str(svg_path), "sha256": sha256(svg_path)}, } audit.exact("HASH-01", "proveniencia", "MD5 do CSV de cursos", source_hashes["course_csv"]["md5"], "4ada15841c698a47ec19d6cb53ac2c13", "Hash da copia local") audit.exact("HASH-02", "proveniencia", "MD5 do CSV de IES", source_hashes["ies_csv"]["md5"], "dbe25e67857aa68b12beaeffa194f53f", "Hash da copia local") dictionary = read_dictionary(dictionary_path, audit) ies = read_ies(ies_path, audit) raw = read_courses(course_path, audit) derived = compare_derived(raw, args.package_root, audit) unknown_ies = sorted({code for label in derived.values() for code, _ in label if code not in ies}) audit.exact("KEY-03", "integridade", "Todos os CO_IES derivados existem na tabela de IES", len(unknown_ies), 0, "Relacionamento CO_IES") points = load_municipal_points(points_path) geometries, bounds, vertices = load_geometries(args.shapefile, audit) point_result = validate_points(raw, points, geometries, audit) svg_result = validate_svg(svg_path, raw, points, bounds, audit) per_uf = uf_counts(raw, geometries) failed = [test for test in audit.tests if test["status"] == "fail"] warnings = [test for test in audit.tests if test["status"] == "warning"] verdict = "VALIDACAO COMPUTACIONAL CONCLUIDA COM LIMITES EXPLICITOS" if not failed else "FALHA DE VALIDACAO: INCONSISTENCIAS ENCONTRADAS" result = { "schema_version": "1.0", "generated_at_utc": datetime.now(timezone.utc).isoformat(), "verdict": verdict, "validation_scope": "Verificacao documental, computacional, topologica e visual desenvolvida no proprio projeto; nao e auditoria externa, revisao por pares nem certificacao institucional.", "validated_construct": "Presenca municipal booleana de oferta por modalidade.", "prohibited_interpretations": [ "ponto como instituicao, campus, unidade ou polo individual", "endereco fisico", "quantidade municipal de cursos, vagas ou inscritos EAD", "precisao inferior ao municipio", ], "authorship": { "public_credit": "Concepcao, visualizacao, design e desenvolvimento: Said Tayar / Ockam Design", "methodology_credit": None, "note": "A metodologia e documentada e verificada computacionalmente no proprio projeto, sem atribuicao autoral individual a Said Tayar.", }, "counts": { "presencial_pairs": len(raw["pairs"]["presencial"]), "ead_pairs": len(raw["pairs"]["EAD"]), "presencial_municipalities": len(raw["presencial_municipalities"]), "ead_municipalities": len(raw["ead_municipalities"]), "combined_municipalities": len(raw["combined_municipalities"]), "both_municipalities": len(raw["both_municipalities"]), "ibge_geocodes": len(geometries), "ibge_vertices": vertices, }, "dimension_counts": raw["dimension_counts"], "dictionary_evidence": dictionary, "source_hashes": source_hashes, "software": { "shapely": __import__("shapely").__version__, "pyproj": __import__("pyproj").__version__, "pyshp": shapefile.__version__, }, "point_sensitivity": point_result, "projection_bounds": bounds, "svg": {**svg_result, "marker_counts": dict(svg_result["marker_counts"])}, "per_uf": per_uf, "tests": audit.tests, "summary": { "tests_total": len(audit.tests), "passed": len(audit.tests) - len(failed) - len(warnings), "warnings": len(warnings), "warning_ids": [test["id"] for test in warnings], "failed": len(failed), "failed_ids": [test["id"] for test in failed], }, } json_path = result_root / "17_validacao_computacional_metodologia.json" md_path = result_root / "17_RELATORIO_VALIDACAO_METODOLOGICA.md" html_path = result_root / "17_RELATORIO_VALIDACAO_METODOLOGICA.html" tests_path = result_root / "17_testes_metodologia.csv" uf_path = result_root / "17_contagens_aferidas_por_uf.csv" json_path.write_text(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") md_path.write_text(report_markdown(result), encoding="utf-8") html_path.write_text(report_html(result), encoding="utf-8") write_tests_csv(tests_path, audit.tests) write_uf_csv(uf_path, per_uf) print(json.dumps({"verdict": verdict, "summary": result["summary"], "outputs": [str(json_path), str(md_path), str(html_path), str(tests_path), str(uf_path)]}, ensure_ascii=False, indent=2)) if failed: raise SystemExit(2) if __name__ == "__main__": main()