Organizar e integrar, de forma sistemática, os dados demográficos derivados do World Population Prospects 2024 (UN-DESA (2024)), estruturando-os em um banco de dados que preserve a lógica original da base da ONU e, ao mesmo tempo, permita sua utilização ampliada em pesquisas acadêmicas e análises comparativas. O banco tem como finalidade apoiar análises demográficas, assegurando clareza quanto às fontes, cenários e métodos de estimativa, e facilitando consultas por tempo, território, indicador, sexo, idade e variante demográfica.
Estruturar os dados de modo a distinguir claramente as dimensões demográficas (tempo, território, indicadores, idade, sexo, variantes, fontes etc.) e os valores numéricos associados a cada combinação dessas dimensões.
Manter os identificadores oficiais da ONU (por exemplo, locationId, indicatorId, timeId), assegurando compatibilidade com a base internacional e permitindo o diálogo direto com a API do WPP, reduzindo ambiguidades conceituais e operacionais.
Possibilitar a incorporação de códigos e nomenclaturas alternativas (como códigos do IBGE e outras INE’s, bem como denominações em português ou espanhol), favorecendo análises comparativas sem comprometer a estrutura internacional de referência.
Registrar informações sobre fonte, variante de projeção, tipo e método de estimativa, permitindo distinguir claramente:
Assegurar que o banco possa ser:
Estruturar o esquema de modo que ele possa incorporar, no futuro:
Disponibilizar uma base organizada que facilite:
O banco armazena dados estruturados e metadados associados, organizando-os para fins analíticos e acadêmicos. Não substitui a documentação metodológica oficial da ONU, mas atua como instrumento de operacionalização e integração dos dados demográficos para pesquisa e ensino.
A modelagem adotada busca organizar os dados demográficos de forma coerente com a lógica analítica do World Population Prospects 2024 (UN-DESA (2024)), permitindo que cada valor seja interpretado como resultado da combinação entre tempo, território, indicador, idade, sexo e variante demográfica.
A separação entre tabelas dimensionais (tempo, localização, indicador, idade, sexo, variante, fonte) e tabela de fatos (valores numéricos) não é apenas uma decisão técnica, mas uma forma de estruturar o fenômeno demográfico como um sistema de medidas observadas em múltiplas dimensões. Essa organização reduz redundâncias, melhora a consistência interna e facilita análises comparativas e longitudinais.
Sempre que possível, preservam-se os identificadores oficiais da ONU (locationId, indicatorId, timeId), garantindo compatibilidade com a API do WPP e evitando ambiguidades decorrentes de variações textuais.
Nesta etapa, o banco é implementado em SQLite como ambiente de prototipagem e validação estrutural. A escolha deve-se à portabilidade, simplicidade de uso e integração direta com o R, permitindo reconstrução integral por meio de script.
A arquitetura, entretanto, foi concebida para futura migração para PostgreSQL com PostGIS, possibilitando maior escalabilidade e integração com dados espaciais, especialmente em aplicações subnacionais.
O esquema foi projetado para permitir:
O banco de dados foi estruturado para organizar os dados demográficos do WPP segundo uma lógica analítica clara, separando valores quantitativos, classificações e informações auxiliares.
A arquitetura está organizada em três camadas complementares:
Essa separação permite distinguir entre os dados utilizados na análise demográfica, as informações que explicam esses dados e os mecanismos técnicos responsáveis pela sua obtenção e atualização.
A Camada Analítica constitui o núcleo do banco.
Ela é composta por:
A tabela de fatos reúne os valores observados ou projetados, enquanto as dimensões definem os recortes analíticos de: - tempo - território - indicador - idade - sexo - variante - tipo de estimativa - método - fonte
Cada registro da tabela de fatos corresponde a um valor demográfico definido por uma combinação específica desses eixos. Essa organização permite análises comparativas entre países, ao longo do tempo, por estrutura etária, sexo e diferentes variantes de projeção.
A Camada de Metadados reúne informações que descrevem, qualificam ou contextualizam os dados analíticos.
Inclui, por exemplo: - definições de indicadores; - descrições conceituais; - unidades de medida; - notas metodológicas; - informações complementares por indicador ou por localização.
Essa camada não contém valores demográficos propriamente ditos, mas fornece o suporte interpretativo necessário para sua correta utilização.
A Camada de Ingestão é responsável pelo processo de obtenção, registro e atualização dos dados.
Ela pode incluir: - armazenamento das respostas brutas da API; - registros de processamento; - tabelas temporárias de validação.
Essa camada não integra o modelo analítico principal, mas garante reprodutibilidade, rastreabilidade e controle das atualizações.
De forma simplificada, a arquitetura pode ser representada como:
Camada de Ingestão -> Camada de Metadados -> Camada Analítica (fatos + dimensões)
A separação em camadas permite que o banco seja utilizado tanto para análise demográfica substantiva quanto para controle metodológico e atualização estruturada.
Esta seção descreve as tabelas que compõem o núcleo analítico do banco de dados. Elas organizam as dimensões estruturais do fenômeno demográfico e armazenam os valores quantitativos observados ou projetados.
Estrutura mínima recomendada:
fact_populationdim_indicatorsdim_topicsdim_sourcesdim_timesdim_locationsdim_agesdim_sexesdim_variantsdim_categoriescomo descrito a seguir
O que é esta tabela: Reúne os valores dos indicadores demográficos organizados por tempo, território, idade, sexo e características do cenário demográfico. Cada linha corresponde a um único valor demográfico definido por uma combinação específica dessas dimensões.
Como os registros são identificados: Cada linha possui um
identificador interno (factId) utilizado apenas para
organização do banco.
Um registro é definido pela combinação de:
Em termos substantivos, isso significa que não pode existir mais de um valor para a mesma combinação dessas características.
Relação com outras tabelas: Cada campo que termina em Id corresponde a uma categoria definida em outra tabela do banco.
Por exemplo:
timeId remete à tabela de períodos (dim_times)locationId remete à tabela de localidades
(dim_locations)indicatorId remete à tabela de indicadores
(dim_indicators)ageId remete à tabela de grupos etários (dim_ages)sexId remete à tabela de sexo (dim_sexes)variantId remete à tabela de variantes
(dim_variants)estimateTypeId remete à tabela de tipos de
estimativaestimateMethodId remete à tabela de métodossourceId remete à tabela de fontesEssas tabelas funcionam como dicionários de classificação.
Campos:
| Tabela: Estrutura de fact_population | ||
| Campo | Tipo | Descrição |
|---|---|---|
| fact_id | BIGINT | Identificador técnico do registro |
| location_id | INTEGER | Unidade territorial |
| indicator_id | INTEGER | Indicador |
| source_id | INTEGER | Fonte dos dados |
| variant_id | INTEGER | Variante de projeção |
| time_id | INTEGER | Referência temporal |
| category_id | INTEGER | Categoria do valor (quando aplicável) |
| estimate_type_id | INTEGER | Tipo de estimativa (Id) |
| estimate_type | TEXT | Tipo de estimativa (histórica, projeção etc.) |
| estimate_method_id | INTEGER | Método de estimação (Id) |
| estimate_method | TEXT | Método de estimação |
| sex_id | INTEGER | Categoria de sexo |
| age_id | INTEGER | Categoria do grupo etário |
| value | REAL | Valor numérico do indicador |
O que é esta tabela: Reúne os atributos próprios dos indicadores disponibilizados pela API. Aqui devem permanecer o nome, a descrição, a forma de apresentação e os parâmetros de formatação e uso do indicador.
Observação de modelagem: Embora o endpoint de indicadores traga também informações de fonte e tema, esses campos devem ser separados em tabelas próprias para evitar repetição.
Campos:
| Tabela: Estrutura de dim_indicators | ||
| Campo | Tipo | Descrição |
|---|---|---|
| indicator_id | INTEGER | Identificador único do indicador na base da ONU |
| indicator | TEXT | Nome completo do indicador |
| short_name | TEXT | Nome abreviado ou código curto do indicador |
| description | TEXT | Descrição conceitual do indicador conforme documentação oficial |
| indicator_display_name | TEXT | Nome utilizado para exibição em tabelas e interfaces |
| dim_age | INTEGER | Indica se o indicador admite desagregação por idade (1 = sim, 0 = não) |
| dim_sex | INTEGER | Indica se o indicador admite desagregação por sexo (1 = sim, 0 = não) |
| dim_variant | INTEGER | Indica se o indicador admite variantes de projeção (1 = sim, 0 = não) |
| dim_category | INTEGER | Indica se o indicador utiliza uma categoria substantiva adicional |
| default_age_id | INTEGER | Grupo etário padrão associado ao indicador |
| default_sex_id | INTEGER | Categoria de sexo padrão associada ao indicador |
| default_variant_id | INTEGER | Variante de projeção padrão associada ao indicador |
| default_category_id | INTEGER | Categoria padrão associada ao indicador, quando aplicável |
| variable_type | TEXT | Natureza substantiva da variável (ex.: absoluta, relativa, duração) |
| value_type | TEXT | Tipo de valor apresentado (ex.: número, percentual) |
| unit_scaling | REAL | Fator de escala aplicado à unidade do indicador |
| precision | INTEGER | Número de casas decimais recomendado para apresentação |
| is_thousand_separator_space | INTEGER | Indica se utiliza espaço como separador de milhar |
| format_string | TEXT | Padrão de formatação numérica recomendado |
| unit_short_label | TEXT | Rótulo curto da unidade de medida |
| unit_long_label | TEXT | Rótulo longo ou descritivo da unidade de medida |
| n_classes_default | INTEGER | Número padrão de classes sugerido para visualizações |
| download_file_name | TEXT | Nome sugerido para arquivo de download do indicador |
| topic_id | INTEGER | Identificador do tema ao qual o indicador pertence |
| indicator_source_id | INTEGER | Identificador da fonte associada à documentação do indicador |
O que é esta tabela: agrupa os temas substantivos aos quais os indicadores pertencem.
Campos sugeridos
topic_idnameshort_namesort_orderO que é esta tabela: Armazena a informação de fonte ou revisão associada aos indicadores e aos dados observados/projetados.
Campos
source_idnamesource_yearstart_yearend_yearcitationurlauthorO que é esta tabela: Define os períodos de referência usados na base.
Campos
time_idlabeltime_starttime_endtime_middurationObservação de modelagem: Se for conveniente para uso
analítico, pode-se acrescentar um campo derivado year, mas
ele não precisa substituir o campo original label.
O que é esta tabela: Reúne as unidades territoriais utilizadas no banco, preservando os identificadores da ONU como referência principal.
Campos: idênticos aos disponíveis em https://population.un.org/wpp/assets/Excel%20Files/4_Metadata/WPP2024_F01_LOCATIONS.xlsx
Observação de modelagem: Nomes em português, espanhol ou códigos nacionais estão em tabelas auxiliares de harmonização, sem alterar a estrutura principal.
O que é esta tabela: Define os grupos etários utilizados pelos indicadores que possuem desagregação por idade.
Campos:
age_idageage_startage_endO que é esta tabela
Armazena as categorias de sexo utilizadas pela API.
Campos
sex_idlabelO que é esta tabela
Armazena as variantes de projeção demográfica.
Campos
variant_idvariant_namevariant_descriptionvariant_sort_orderO que é esta tabela: Armazena categorias substantivas
associadas a alguns indicadores. Certos indicadores usam categoria
(dimCategory = true) e definem também uma categoria padrão
(defaultCategoryId).
Campos
category_idcategory_namecategory_sort_orderlibrary(DBI)
library(RSQLite)
library(here)
db_path <- here::here("data", "pop", "population_data.sqlite")
create_newdb <- function(db_path, overwrite = FALSE) {
if (file.exists(db_path) & overwrite) {
file.remove(db_path)
}
db <- dbConnect(RSQLite::SQLite(), db_path)
dbExecute(db, "PRAGMA foreign_keys = ON;")
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_topics (
topic_id INTEGER NOT NULL,
name TEXT NOT NULL,
short_name TEXT,
sort_order INTEGER,
PRIMARY KEY (topic_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_sources (
source_id INTEGER NOT NULL,
name TEXT NOT NULL,
source_year INTEGER,
start_year INTEGER,
end_year INTEGER,
citation TEXT,
url TEXT,
author TEXT,
PRIMARY KEY (source_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_times (
time_id INTEGER NOT NULL,
time_label TEXT NOT NULL,
time_start REAL,
time_end REAL,
time_mid REAL,
duration REAL,
PRIMARY KEY (time_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_locations (
idx INTEGER,
location TEXT NOT NULL,
notes TEXT,
location_id INTEGER NOT NULL,
location_iso3code TEXT,
location_iso2code TEXT,
sdmx_code TEXT,
loc_type INTEGER,
loc_type_name TEXT,
parent_id INTEGER,
world_id INTEGER,
subreg_id INTEGER,
subreg_name TEXT,
georeg_id INTEGER,
georeg_name TEXT,
more_dev INTEGER,
less_dev INTEGER,
least_dev INTEGER,
o_less_dev INTEGER,
less_dev_excluding_china INTEGER,
PRIMARY KEY (location_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_ages (
age_id INTEGER NOT NULL,
age TEXT NOT NULL,
age_start INTEGER,
age_end INTEGER,
PRIMARY KEY (age_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_sexes (
sex_id INTEGER NOT NULL,
sex TEXT NOT NULL,
sex_sort_order INTEGER NOT NULL,
PRIMARY KEY (sex_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_variants (
variant_id INTEGER NOT NULL,
name TEXT NOT NULL,
label TEXT,
short_name TEXT,
uncertainty INTEGER,
prediction_interval_label TEXT,
uncertainty_z_index INTEGER,
sort_order INTEGER,
description TEXT,
PRIMARY KEY (variant_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_categories (
category_id INTEGER NOT NULL,
category_name TEXT NOT NULL,
category_sort_order INTEGER,
PRIMARY KEY (category_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_estimate_types (
estimate_type_id INTEGER NOT NULL,
estimate_type_name TEXT NOT NULL,
PRIMARY KEY (estimate_type_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_estimate_methods (
estimate_method_id INTEGER NOT NULL,
estimate_method_name TEXT NOT NULL,
PRIMARY KEY (estimate_method_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS dim_indicators (
indicator_id INTEGER NOT NULL,
indicator TEXT NOT NULL,
short_name TEXT,
description TEXT,
indicator_display_name TEXT,
dim_age INTEGER,
dim_sex INTEGER,
dim_variant INTEGER,
dim_category INTEGER,
default_age_id INTEGER,
default_sex_id INTEGER,
default_variant_id INTEGER,
default_category_id INTEGER,
variable_type TEXT,
value_type TEXT,
unit_scaling REAL,
precision INTEGER,
is_thousand_separator_space INTEGER,
format_string TEXT,
unit_short_label TEXT,
unit_long_label TEXT,
n_classes_default INTEGER,
download_file_name TEXT,
topic_id INTEGER,
indicator_source_id INTEGER,
PRIMARY KEY (indicator_id),
FOREIGN KEY (topic_id) REFERENCES dim_topics(topic_id),
FOREIGN KEY (indicator_source_id) REFERENCES dim_sources(source_id),
FOREIGN KEY (default_age_id) REFERENCES dim_ages(age_id),
FOREIGN KEY (default_sex_id) REFERENCES dim_sexes(sex_id),
FOREIGN KEY (default_variant_id) REFERENCES dim_variants(variant_id),
FOREIGN KEY (default_category_id) REFERENCES dim_categories(category_id)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS fact_population (
fact_id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
indicator_id INTEGER NOT NULL,
source_id INTEGER NOT NULL,
variant_id INTEGER NOT NULL,
time_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
estimate_type_id INTEGER NOT NULL,
estimate_method_id INTEGER NOT NULL,
sex_id INTEGER NOT NULL,
age_id INTEGER NOT NULL,
value REAL NOT NULL,
FOREIGN KEY (location_id) REFERENCES dim_locations(location_id),
FOREIGN KEY (indicator_id) REFERENCES dim_indicators(indicator_id),
FOREIGN KEY (source_id) REFERENCES dim_sources(source_id),
FOREIGN KEY (variant_id) REFERENCES dim_variants(variant_id),
FOREIGN KEY (time_id) REFERENCES dim_times(time_id),
FOREIGN KEY (category_id) REFERENCES dim_categories(category_id),
FOREIGN KEY (estimate_type_id) REFERENCES dim_estimate_types(estimate_type_id),
FOREIGN KEY (estimate_method_id) REFERENCES dim_estimate_methods(estimate_method_id),
FOREIGN KEY (sex_id) REFERENCES dim_sexes(sex_id),
FOREIGN KEY (age_id) REFERENCES dim_ages(age_id),
UNIQUE (
location_id,
indicator_id,
source_id,
variant_id,
time_id,
category_id,
estimate_type_id,
estimate_method_id,
sex_id,
age_id
)
);"
)
dbExecute(
db,
"CREATE TABLE IF NOT EXISTS ingestion_log (
ingestion_id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
endpoint TEXT,
request_time TEXT NOT NULL,
load_time TEXT,
status TEXT NOT NULL,
n_rows_raw INTEGER,
n_rows_inserted INTEGER,
script_name TEXT,
query_params TEXT,
response_hash TEXT,
notes TEXT,
location_id INTEGER,
time_id INTEGER,
indicator_id INTEGER
);"
)
tables <- dbListTables(db)
dbDisconnect(db)
return(tables)
}
create_newdb(db_path, overwrite = TRUE)
log_ingestion <- function(db,
table_name,
endpoint = NA_character_,
request_time = Sys.time(),
load_time = Sys.time(),
status = "success",
n_rows_raw = NA_integer_,
n_rows_inserted = NA_integer_,
script_name = NA_character_,
query_params = NA_character_,
response_hash = NA_character_,
notes = NA_character_,
location_id = NA_integer_,
time_id = NA_integer_,
indicator_id = NA_integer_) {
log_entry <- tibble::tibble(
table_name = table_name,
endpoint = endpoint,
request_time = as.character(request_time),
load_time = as.character(load_time),
status = status,
n_rows_raw = n_rows_raw,
n_rows_inserted = n_rows_inserted,
script_name = script_name,
query_params = query_params,
response_hash = response_hash,
notes = notes,
location_id = location_id,
time_id = time_id,
indicator_id = indicator_id
)
DBI::dbWriteTable(
conn = db,
name = "ingestion_log",
value = log_entry,
append = TRUE
)
}
library(digest)
library(dplyr)
library(readr)
library(here)
HEADER <- c("Authorization" = Sys.getenv("WPP_ONU_BEARER"))
endpoint <- "https://population.un.org/dataportalapi/api/v1/indicators"
request_time <- Sys.time()
raw_json <- RCurl::getURL(
endpoint,
.opts = list(httpheader = HEADER, followlocation = TRUE)
)
response_hash <- digest(raw_json, algo = "sha256")
indicators = jsonlite::fromJSON(raw_json)$data |>
rename(
indicator_id = id,
indicator = name,
indicator_display_name = displayName,
indicator_source_id = sourceId
) |>
select(
-sourceName, -sourceYear, -sourceStartYear, -sourceEndYear,
-sourceCitation, -sourceUrl, -topicName, -topicShortName
)
names(indicators) <- snakecase::to_snake_case(names(indicators))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_indicators",
value = indicators,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_indicators",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(indicators),
n_rows_inserted = nrow(indicators),
script_name = "create_database.mdR chunk dim_indicators",
response_hash = response_hash,
notes = "Carga inicial de indicadores"
)
dbDisconnect(db)
destfile_onu <- here::here("data","pop", "WPP2024_F01_LOCATIONS.xlsx")
endpoint <- 'https://population.un.org/wpp/assets/Excel%20Files/4_Metadata/WPP2024_F01_LOCATIONS.xlsx'
request_time <- Sys.time()
# download se necessário
# download.file(endpoint, destfile = destfile_onu, mode = "wb")
response_hash <- digest::digest(file = destfile_onu, algo = "sha256")
locations = readxl::read_xlsx(destfile_onu, sheet = 'DB')
names(locations) = snakecase::to_snake_case(names(locations))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
locations = locations %>%
rename(idx= index,
location_id = loc_id,
location_iso2code = iso_2_code,
location_iso3code = iso_3_code,
subreg_id = sub_reg_id,
subreg_name = sub_reg_name,
georeg_id = geo_reg_id,
georeg_name = geo_reg_name) %>%
select(all_of(dbListFields(db, 'dim_locations')))
dbWriteTable(conn = db,
name = "dim_locations",
value = locations,
append = TRUE)
log_ingestion(
db = db,
table_name = "dim_locations",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(locations),
n_rows_inserted = nrow(locations),
script_name = "create_database.mdR chunk dim_locations",
response_hash = response_hash,
notes = "Carga inicial dos locais"
)
dbDisconnect(db)
endpoint <- 'https://population.un.org/dataportalapi/api/v1/dimensions/times/None'
request_time <- Sys.time()
raw_json <- RCurl::getURL(
endpoint,
.opts = list(httpheader = HEADER, followlocation = TRUE)
)
response_hash <- digest(raw_json, algo = "sha256")
times = jsonlite::fromJSON(raw_json) |>
rename(time_id = id,
time_label = label)
names(times) = snakecase::to_snake_case(names(times))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_times",
value = times,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_times",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(times),
n_rows_inserted = nrow(times),
script_name = "create_database.mdR chunk dim_times",
response_hash = response_hash,
notes = "Carga inicial dos períodos"
)
dbDisconnect(db)
endpoint <- 'https://population.un.org/dataportalapi/api/v1/sources'
request_time <- Sys.time()
raw_json <- RCurl::getURL(
endpoint,
.opts = list(httpheader = HEADER, followlocation = TRUE)
)
response_hash <- digest(raw_json, algo = "sha256")
sources = jsonlite::fromJSON(raw_json)$data |>
rename(sourceId = id)
names(sources) = snakecase::to_snake_case(names(sources))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_sources",
value = sources,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_sources",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(sources),
n_rows_inserted = nrow(sources),
script_name = "create_database.mdR chunk dim_sources",
response_hash = response_hash,
notes = "Carga inicial das fontes"
)
dbDisconnect(db)
#create_newdb(DB_PATH)
endpoint <- 'https://population.un.org/dataportalapi/api/v1/dimensions/variants'
request_time <- Sys.time()
raw_json <- RCurl::getURL(
endpoint,
.opts = list(httpheader = HEADER, followlocation = TRUE)
)
response_hash <- digest(raw_json, algo = "sha256")
variants = jsonlite::fromJSON(raw_json) |>
rename(variantId = id)
names(variants) = snakecase::to_snake_case(names(variants))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_variants",
value = variants,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_variants",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(variants),
n_rows_inserted = nrow(variants),
script_name = "create_database.mdR chunk dim_variants",
response_hash = response_hash,
notes = "Carga inicial das variantes"
)
dbDisconnect(db)
#create_newdb(DB_PATH)
endpoint <- 'https://population.un.org/dataportalapi/api/v1/topics'
request_time <- Sys.time()
raw_json <- RCurl::getURL(
endpoint,
.opts = list(httpheader = HEADER, followlocation = TRUE)
)
response_hash <- digest(raw_json, algo = "sha256")
topics = jsonlite::fromJSON(raw_json)$data |>
rename(topicId = id)
names(topics) = snakecase::to_snake_case(names(topics))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_topics",
value = topics,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_variants",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(topics),
n_rows_inserted = nrow(topics),
script_name = "create_database.mdR chunk dim_variants",
response_hash = response_hash,
notes = "Carga inicial das variantes"
)
dbDisconnect(db)
#create_newdb(DB_PATH)
endpoint <- paste0('https://population.un.org/dataportalapi/api/v1/metadata/ages/',
paste(indicators$indicator_id, collapse = ","))
request_time <- Sys.time()
raw_json <- RCurl::getURL(
endpoint,
.opts = list(httpheader = HEADER, followlocation = TRUE)
)
response_hash <- digest(raw_json, algo = "sha256")
ages = jsonlite::fromJSON(raw_json) |>
select(-indicatorId, -indicatorName)|>
distinct(ageId, .keep_all = TRUE)
names(ages) = snakecase::to_snake_case(names(ages))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_ages",
value = ages,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_ages",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(ages),
n_rows_inserted = nrow(ages),
script_name = "create_database.mdR chunk dim_ages",
response_hash = response_hash,
notes = "Carga inicial das idades"
)
dbDisconnect(db)
#create_newdb(DB_PATH)
endpoint <- 'Tibble gerada Manualmente (muito mais simples)'
request_time <- Sys.time()
sexes = tibble(sexId = 1:3,
sex = c('Male','Female','Both sexes'),
sexSortOrder = c(3,1,2))
names(sexes) = snakecase::to_snake_case(names(sexes))
response_hash <- digest(sexes, algo = "sha256")
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_sexes",
value = sexes,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_sexes",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(sexes),
n_rows_inserted = nrow(sexes),
script_name = "create_database.mdR chunk dim_sexes",
response_hash = response_hash,
notes = "Carga inicial dos sexos"
)
dbDisconnect(db)
#create_newdb(DB_PATH)
endpoint <- paste0('https://population.un.org/dataportalapi/api/v1/metadata/categories/',
paste(indicators$indicator_id, collapse = ","))
request_time <- Sys.time()
raw_json <- RCurl::getURL(
endpoint,
.opts = list(httpheader = HEADER, followlocation = TRUE)
)
response_hash <- digest(raw_json, algo = "sha256")
categories = jsonlite::fromJSON(raw_json) |>
select(-indicatorId, -indicatorName)|>
distinct(categoryId, .keep_all = TRUE)
names(categories) = snakecase::to_snake_case(names(categories))
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
dbWriteTable(conn = db,
name = "dim_categories",
value = categories,
append = TRUE)
# registra processo
log_ingestion(
db = db,
table_name = "dim_categories",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = "success",
n_rows_raw = nrow(categories),
n_rows_inserted = nrow(categories),
script_name = "create_database.mdR chunk dim_categories",
response_hash = response_hash,
notes = "Carga inicial das categorias"
)
dbDisconnect(db)
BASE_URL <- "https://population.un.org/dataportalapi/api/v1"
HEADER <- c("Authorization" = Sys.getenv("WPP_ONU_BEARER"))
DB_PATH <- here::here("data", "pop", "population_data.sqlite")
source(here::here('R/download_pages_WPP.R'))
# SQL simples com essas constantes aí em cima
sql_basic = function(query){
con <- dbConnect(RSQLite::SQLite(), DB_PATH)
df = dbGetQuery(con, query)
dbDisconnect(con)
return(df)
}
get_dim_value <- function(table_name, id_column, id_value, return_column, db_path = DB_PATH) {
db <- DBI::dbConnect(RSQLite::SQLite(), db_path)
on.exit(DBI::dbDisconnect(db), add = TRUE)
sql <- paste0(
"SELECT ", return_column,
" FROM ", table_name,
" WHERE ", id_column, " = ", id_value
)
result <- DBI::dbGetQuery(db, sql)
if (nrow(result) == 0) {
return(NA)
}
result[[return_column]][1]
}
# Função para processar dados de um local e ano específico
process_location_data <- function(location_id, time_id, indicator_id) {
time_label <- get_dim_value("dim_times", "time_id", time_id, "time_label")
target <- paste0(BASE_URL,
"/data/indicators/", indicator_id,
"/locations/", location_id,
"/start/", time_label, "/end/", time_label,
"?pagingInHeader=false&format=json")
# Fazer o download e processar os dados
download_pages_WPP(target, HEADER)
}
insert_if_not_exists <- function(conn, table_name, data, key_columns) {
existing_data <- dbReadTable(conn, table_name)
new_data <- dplyr::anti_join(data, existing_data, by = key_columns)
if (nrow(new_data) > 0) {
dbWriteTable(conn, table_name, new_data, append = TRUE, row.names = FALSE)
}
}
check_missing_keys <- function(db, table_name, key_column, values) {
values <- unique(values[!is.na(values)])
if (length(values) == 0) {
return(integer(0))
}
sql <- paste0(
"SELECT ", key_column, " FROM ", table_name,
" WHERE ", key_column, " IN (", paste(values, collapse = ","), ")"
)
existing <- DBI::dbGetQuery(db, sql)[[1]]
setdiff(values, existing)
}
save_dimensions <- function(data, db) {
# -------------------------
# 1. Validar dimensões pré-carregadas
# -------------------------
missing_sources <- check_missing_keys(db, "dim_sources", "source_id", data$sourceId)
missing_variants <- check_missing_keys(db, "dim_variants", "variant_id", data$variantId)
missing_times <- check_missing_keys(db, "dim_times", "time_id", data$timeId)
missing_sexes <- check_missing_keys(db, "dim_sexes", "sex_id", data$sexId)
missing_ages <- check_missing_keys(db, "dim_ages", "age_id", data$ageId)
missing_categories <- check_missing_keys(db, "dim_categories", "category_id", data$categoryId)
if (length(missing_sources) > 0) {
stop("source_id ausente em dim_sources: ", paste(missing_sources, collapse = ", "))
}
if (length(missing_variants) > 0) {
stop("variant_id ausente em dim_variants: ", paste(missing_variants, collapse = ", "))
}
if (length(missing_times) > 0) {
stop("time_id ausente em dim_times: ", paste(missing_times, collapse = ", "))
}
if (length(missing_sexes) > 0) {
stop("sex_id ausente em dim_sexes: ", paste(missing_sexes, collapse = ", "))
}
if (length(missing_ages) > 0) {
stop("age_id ausente em dim_ages: ", paste(missing_ages, collapse = ", "))
}
if (length(missing_categories) > 0) {
stop("category_id ausente em dim_categories: ", paste(missing_categories, collapse = ", "))
}
# -------------------------
# 2. Inserir dimensões residuais
# -------------------------
dim_estimate_types <- data |>
dplyr::select(
estimate_type_id = estimateTypeId,
estimate_type_name = estimateType
) |>
dplyr::distinct()
insert_if_not_exists(
db,
"dim_estimate_types",
dim_estimate_types,
key_columns = "estimate_type_id"
)
dim_estimate_methods <- data |>
dplyr::select(
estimate_method_id = estimateMethodId,
estimate_method_name = estimateMethod
) |>
dplyr::distinct()
insert_if_not_exists(
db,
"dim_estimate_methods",
dim_estimate_methods,
key_columns = "estimate_method_id"
)
}
save_fact_population <- function(data, db) {
fact_population <- data |>
select(
locationId,
indicatorId,
sourceId,
variantId,
timeId,
categoryId,
estimateTypeId,
estimateMethodId,
sexId,
ageId,
value
)|>
dplyr::rename_with(snakecase::to_snake_case)
dbWriteTable(
conn = db,
name = "fact_population",
value = fact_population,
append = TRUE,
row.names = FALSE
)
}
save_processing_log <- function(location_id,
time_id,
indicatorId,
db,
endpoint = NA_character_,
request_time = Sys.time(),
response_hash = NA_character_,
status = "success") {
log_ingestion(
db = db,
table_name = "fact_population",
endpoint = endpoint,
request_time = request_time,
load_time = Sys.time(),
status = status,
n_rows_raw = NA_integer_,
n_rows_inserted = NA_integer_,
script_name = "main_insert",
query_params = paste0(
"locationId=", location_id,
";timeId=", time_id,
";indicatorId=", indicator_id
),
response_hash = response_hash,
notes = "Processamento de recorte analítico",
location_id = location_id,
time_id = time_id,
indicator_id = indicator_id
)
}
check_processing_log <- function(time_id, location_id, indicator_id, db) {
sql_query <- paste0(
"SELECT COUNT(*) AS count
FROM ingestion_log
WHERE table_name = 'fact_population'
AND time_id = ", time_id,
" AND location_id = ", location_id,
" AND indicator_id = ", indicator_id,
" AND status = 'success'"
)
query <- DBI::dbGetQuery(db, sql_query)
if (is.null(query$count) || is.na(query$count)) {
return(FALSE)
}
query$count > 0
}
insert_all_data <- function(locations, times, indicators, db) {
for (t in seq_len(nrow(times))) {
time_label <- times[t, ]$time_label
time_id <- times[t, ]$time_id
for (l in seq_len(nrow(locations))) {
location <- locations[l, ]$location
location_id <- locations[l, ]$location_id
for (i in seq_len(nrow(indicators))) {
indicator <- indicators[i, ]$indicator
indicator_id <- indicators[i, ]$indicator_id
cat(
paste0(
"\nProcessing Year: ", time_label, " (", time_id, ")",
" - Location: ", location, " (", location_id, ")",
" - Indicator: ", indicator, " (", indicator_id, ")\n"
)
)
if (check_processing_log(time_id, location_id, indicator_id, db)) {
cat(
paste0(
">>>>> Skipping: Year: ", time_label, " (", time_id, ")",
" - Location: ", location, " (", location_id, ")",
" - Indicator: ", indicator, " (", indicator_id, ")\n"
)
)
next
}
wpp_raw <- process_location_data(location_id, time_id, indicator_id)
save_dimensions(wpp_raw, db)
save_fact_population(wpp_raw, db)
save_processing_log(location_id, time_id, indicator_id, db)
}
}
}
}
# ----
#EXECUÇÃO DO PROGRAMA
main_insert <- function(locations, times, indicators) {
db <- dbConnect(RSQLite::SQLite(), DB_PATH)
insert_all_data(locations, times, indicators, db)
dbDisconnect(db)
cat("\nProcessing completed successfully!\n")
}
Location
indicador
Períodos
source(here('R/download_pages_WPP.R'))
locations <- sql_basic("SELECT * FROM dim_locations WHERE location_id IN (900)")
indicators = sql_basic("SELECT * FROM dim_indicators WHERE indicator_id IN (46,49)")
years <- c(1950, 2000, 2050, 2100)
query = paste0("SELECT * FROM dim_times WHERE time_label IN ('",
paste(years, collapse = "', '"), "')")
times <- sql_basic(query)
main_insert(locations, times, indicators)
#knitr::purl('data/pop/create_database.Rmd', output = 'data/pop/create_database.r')