3 · Preparação dos dados¶
Limpeza:
python -m airbnb.clean.pipeline(src/airbnb/clean/). Números dedata/processed/_qualidade.json. O contrato de cada coluna está no Dicionário de dados, gerado desrc/airbnb/schema.py.
1. Bronze → silver¶
saída (data/processed/) |
linhas × colunas | conteúdo |
|---|---|---|
anuncios_2019.parquet |
48.895 × 25 | Kaggle limpo e tipado, sem host_name, + derivadas |
anuncios_2026.parquet |
30.257 × 102 | detalhado limpo e tipado, sem colunas pessoais nem texto livre, + derivadas |
anuncios_resumo.parquet |
79.152 × 23 | os dois snapshots empilhados nas colunas comuns + snapshot |
quarentena.parquet |
9.156 × 5 | toda linha excluída — da base ou do preço — com regra, motivo e escopo |
_qualidade.json |
— | regras × snapshot, cascata, nulos antes/depois, hash das entradas e das saídas |
Por que 2026 vem do detalhado, e não do resumo. O resumo (visualisations/listings.csv)
tem 30.555 linhas; o detalhado, 30.259. O detalhado cobre todos os ids que tem em
comum com o resumo com valores idênticos em todas as colunas comuns (coordenada,
bairro, tipo, mínimo de noites, avaliações, disponibilidade, licença: 100%); o preço
do resumo é o do detalhado arredondado ao dólar (diferença máxima de US$ 0,50). Os
296 ids que só o resumo tem são todos sem host_id, Private room, mínimo de 1
noite e sem licença; 268 têm nome de hotel ("… Hotel, Deluxe King"), 246 estão em
Manhattan, preço mediano de US$ 249. É inventário de hotel distribuído sem perfil de
anfitrião — o detalhado não o traz, e ele não teria nenhuma das 71 colunas extras.
Ficar com o detalhado custa 296 quartos de hotel (1% do resumo) e ganha cotação,
amenidades, licença, ocupação estimada e notas.
Tipagem. Cada coluna recebe o dtype do contrato (schema.tipos): datas como
datetime64, t/f como booleano anulável, categorias com domínio fechado. A
conversão não remenda: valor fora do domínio de uma categoria e booleano com nulo
são erro (o pd.Categorical transformaria o valor estranho em nulo, e o
astype(bool) transformaria nulo em True). Ao fim, schema.validar confere colunas,
ordem, dtypes, domínios e nulos proibidos das três saídas; se algo divergir, nada é
gravado.
2. Regras e quarentena¶
Invariante 2: nenhuma linha sai em silêncio. Há dois escopos, e a diferença importa:
base— a linha sai de tudo: não descreve um anúncio válido.preco— a linha fica (conta anúncio, ocupação, mapa), mas sai das análises de preço:preco_valido = False.
As regras rodam em cascata, na ordem da tabela; cada linha é atribuída à primeira regra em que cai, e a soma fecha com o total. "Casam" conta quem a regra pegaria sozinha.
| regra | escopo | motivo | 2019 casam · cascata | 2026 casam · cascata |
|---|---|---|---|---|
B01 id_duplicado |
base | id repetido (mantida a 1ª ocorrência) | 0 · 0 | 0 · 0 |
B02 duplicata_perfeita |
base | idêntica a outra linha em tudo, exceto o id | 0 · 0 | 0 · 0 |
B03 coordenada_fora_nyc |
base | coordenada nula ou fora do envelope dos 5 distritos | 0 · 0 | 0 · 0 |
B04 distrito_invalido |
base | distrito fora dos 5 | 0 · 0 | 0 · 0 |
B05 bairro_desconhecido |
base | bairro fora da lista do Inside Airbnb | 0 · 0 | 0 · 0 |
B06 room_type_invalido |
base | tipo de acomodação fora do domínio | 0 · 0 | 0 · 0 |
B07 minimo_noites_invalido |
base | mínimo de noites nulo ou < 1 | 0 · 0 | 2 · 2 |
P01 preco_ausente |
preco | preço nulo (sem cotação) | 0 · 0 | 8.744 · 8.743 |
P02 preco_zero |
preco | preço igual a zero | 11 · 11 | 0 · 0 |
P03 cotacao_acima_31_noites |
preco | cotação > 31 noites: total mensal ÷ N noites | — | 413 · 388 |
P04 preco_abaixo_piso |
preco | 0 < preço < US$ 10 | 0 · 0 | 3 · 0 |
P05 preco_acima_teto |
preco | preço > US$ 10.000 | 0 · 0 | 12 · 12 |
As regras de base que não pegaram nada ficam — são a prova de que a condição foi verificada, e disparam se um snapshot futuro trouxer o problema. Os 2 anúncios de B07 não têm mínimo de noites: sem ele não há como situá-los antes ou depois da Local Law 18 (invariante 8).
3. Limiares de preço, justificados com o dado¶
Nenhum corte por percentil: cada limiar tem uma razão observável.
- Piso de US$ 10 (P04). Em 2019 não existe preço entre US$ 1 e US$ 9 — os 11 abaixo de 10 são zeros (P02). Em 2026 os 3 casos abaixo de US$ 10 (mínimo: US$ 4,58) são todos cotações de 180 a 365 noites — já capturados por P03, que é a causa.
- Teto de US$ 10.000 (P05). O máximo de 2019 é exatamente US$ 10.000 (3 anúncios, mais 3 a US$ 9.999): acima disso não há suporte de comparação com 2019. Em 2026, 12 anúncios passam do teto (até US$ 30.972,96 por noite num apartamento de 7 quartos cotado para 30 noites).
- Cotação acima de 31 noites (P03). Até 31 noites o total da cotação é do período; acima disso o Airbnb devolve um total mensal e o Inside Airbnb o divide por N — o preço por noite mediano cai de US$ 144,57 (28–31 noites) para US$ 25,62 (121–365) enquanto o total mediano fica em US$ 4.300–5.500 (Entendimento dos dados, Q5). Uma correção (total ÷ 30) seria defensável, mas depende de inferir a semântica da API; a exclusão do preço é conservadora e reversível.
4. Cascata de exclusões¶
| 2019 | 2026 | |
|---|---|---|
| linhas no bruto | 48.895 | 30.259 |
− escopo base |
0 | 2 (B07) |
| na silver | 48.895 | 30.257 |
| − preço ausente | 0 | 8.743 |
| − preço zero | 11 | 0 |
| − cotação > 31 noites | — | 388 |
| − abaixo do piso | 0 | 0 |
| − acima do teto | 0 | 12 |
| com preço válido | 48.884 | 21.114 |
O código confere que a cascata fecha com a silver (assert em pipeline._cascata).
Nulos antes e depois. A limpeza não imputa nada: todo nulo da silver já estava no
bruto. Em 2019 os nulos são os mesmos (name 16; last_review e reviews_per_month
10.052, estruturais); host_name (21) saiu com a coluna. Em 2026 saem as 12 colunas
100% vazias; banheiros reduz o nulo de 10.110 (bathrooms) para 36 ao completar pelo
texto. Contagem coluna a coluna em _qualidade.json → nulos.
5. Colunas derivadas¶
Regras em src/airbnb/clean/derivadas.py (funções puras, testadas sem o dado real).
Comuns aos dois snapshots¶
| coluna | regra |
|---|---|
snapshot |
"2019" ou "2026" (config.ROTULO_*) |
preco |
price como número: "$1,234.00" → 1234.0 (2019 já é inteiro) |
preco_valido |
não caiu em nenhuma regra de escopo preco |
min30 |
minimum_nights ≥ 30 (config.NOITES_CURTA_TEMPORADA) |
host_multi |
calculated_host_listings_count > 1 |
h3_r9, h3_r8, h3_r6 |
h3.latlng_to_cell(lat, lon, res) nas resoluções de config (features, mapa, blocos de CV) |
dias_desde_ultima_avaliacao |
dias entre last_review e o scrape (2019: config.SNAPSHOT_2019; 2026: last_scraped) |
ocupacao_modelo |
modelo de avaliações do Inside Airbnb, igual nos dois anos: reviews_per_month × 12 ÷ 0,5 × max(6,4; mínimo) ÷ 365, teto 0,70, zero sem avaliação em 365 dias (§4 da fase 2) |
presente_2026 (só 2019) |
o id ainda aparece no snapshot de 2026 (7.516 anúncios) |
Só 2026¶
| coluna | regra |
|---|---|
preco_cotacao_noites |
noites da cotação (checkout − checkin) |
preco_cheio |
diária sem desconto: (total da cotação + |itens negativos|) ÷ noites; coincide até US$ 1 com o item "Average monthly price" em 16.486 de 16.515 cotações (as 29 restantes têm imposto no total) |
desconto_pct |
|itens negativos| ÷ preço cheio do período; 0 sem item de desconto; nulo sem cotação ou com JSON malformado |
banheiros |
bathrooms quando existe; senão o número do texto; "half-bath" = 0,5. Número e texto concordam em 99,995% das 20.024 linhas que têm os dois |
banheiro_compartilhado |
"shared" no bathrooms_text (7.292 anúncios); nulo sem texto (159) |
n_amenidades + 17 amen_* |
contagem e flags da lista JSON (abaixo) |
licenca_status |
registrada (começa com OSE-STRREG, sem distinguir caixa: 2.329) · isenta ("Exempt": 2.956) · ausente (24.972) · outra (0) |
host_superhost |
host_is_superhost com nulo = False (349 anúncios sem perfil) |
host_anos |
hosts_time_as_host_years + months ÷ 12 — host_since veio 100% vazio |
meses_desde_primeira_avaliacao |
(last_scraped − first_review) em dias ÷ 30,4375 |
tipo_imovel_grupo |
69 property_type em 7 grupos, por regra de texto com o room_type como rede de segurança |
tipo_imovel_grupo: apartamento inteiro 14.711 · quarto em apartamento 7.413 · quarto
em casa 3.739 · hotel ou pousada 2.122 · casa inteira 2.005 · quarto compartilhado
218 · atípico (barco, trailer, torre, domo…) 49.
Não derivadas, por falta de fonte: taxas de resposta e de aceitação do anfitrião
(0–1) — host_response_rate e host_acceptance_rate vieram 100% vazias em 2026.
Amenidades: critério de escolha¶
A lista de 2026 tem 6.179 itens distintos, com a mesma coisa grafada de várias formas
("Washer", "Free washer – In unit", "Paid washer – In building"). Cada flag é uma
expressão regular sobre cada item (em schema.AMENIDADES), testada contra as
armadilhas da grafia: "Hair dryer" não é secadora, "Dishwasher" não é lavadora, "Pool
table" não é piscina, "Shared gym nearby" não é academia, "Free street parking" não é
estacionamento do imóvel, "Movie theater" não é aquecedor.
Critério: (1) atributo do imóvel ou do prédio com efeito plausível sobre o preço
(conforto, serviço do prédio, uso por estadia longa); (2) fora consumíveis (xampu,
cabides) e itens exigidos por lei em NYC (detector de fumaça e de CO), que não
diferenciam anúncio; (3) prevalência de pelo menos 1%. Razão = preço mediano com a
amenidade ÷ sem, entre preços válidos, por estrato de mínimo de noites (o price
de 2026 não é a mesma grandeza nos dois). É razão bruta, sem controle — indica
relevância, não efeito: academia e porteiro andam junto com prédio de alto padrão em
Manhattan, e o modelo de preço é quem separa.
| flag | prevalência | razão, mín. < 30 | razão, mín. ≥ 30 |
|---|---|---|---|
amen_wifi |
98,5% | 1,16 | 1,01 |
amen_ar_condicionado |
90,8% | 1,19 | 1,82 |
amen_cozinha |
88,0% | 0,90 | 1,26 |
amen_aquecimento |
87,0% | 1,05 | 1,15 |
amen_tv |
80,5% | 1,61 | 1,96 |
amen_espaco_trabalho |
51,7% | 1,10 | 1,18 |
amen_lavadora |
45,6% | 1,30 | 1,48 |
amen_self_checkin |
45,5% | 0,99 | 1,39 |
amen_secadora |
39,7% | 1,30 | 1,49 |
amen_lava_loucas |
26,9% | 1,36 | 1,83 |
amen_permite_pets |
25,4% | 1,28 | 1,83 |
amen_elevador |
24,1% | 1,21 | 1,82 |
amen_varanda_patio |
19,9% | 0,91 | 1,18 |
amen_academia |
19,2% | 1,44 | 2,00 |
amen_estacionamento |
18,2% | 1,15 | 1,10 |
amen_porteiro |
10,2% | 1,42 | 2,68 |
amen_piscina |
2,2% | 1,43 | 2,78 |
Wi-Fi é quase constante (98,5%) e quase não discrimina no estrato ≥ 30: fica por ter sido pedida e por sinalizar anúncio incompleto quando falta, mas o modelo pode descartá-la.
6. Dado pessoal¶
Sai na limpeza (invariante 9): host_name, host_about, host_location,
host_picture_url, host_thumbnail_url, host_url, host_profile_url,
host_profile_id, host_neighbourhood, listing_url, picture_url, description,
neighborhood_overview — lista em schema.PROIBIDAS_NA_SILVER, conferida por teste
contra os parquets reais.
Fica, marcado pessoal = True (nunca publicado): id e host_id (cruzar snapshots,
medir concentração) e name, o título do anúncio (análise de duplicatas).
amenities fica: é lista de comodidades, não texto do anfitrião.
7. Reprodutibilidade¶
- Idempotente: duas execuções completas produziram o mesmo hash de conteúdo nas
quatro saídas (
_qualidade.json→saidas); teste com frames sintéticos trava isso. - Determinística: nenhuma amostragem; a ordem das linhas é a do arquivo de origem.
- Rastreável:
_qualidade.jsonguarda o SHA-256 de cada arquivo de entrada (do manifesto) e o hash de conteúdo de cada saída. - Testada:
tests/test_limpeza.pyetests/test_schema.pyrodam sem o dado real; os testes marcadosdadosconferem os parquets reais contra o contrato.
8. Junção com as features de localização¶
A silver de anúncios é unida às features de localização pela célula H3 r9
(h3_r9, ADR 0001) em src/airbnb/features/anuncios.py,
que produz as tabelas de modelagem anúncio × célula (modelagem_2019.parquet,
modelagem_2026.parquet) e o resumo data/processed/_features.json.
Três decisões:
- A localização é a da célula, não a do anúncio. O anúncio herda as features da célula r9 em que cai — inclusive latitude e longitude, que no modelo são as do centroide da célula, e o distrito, que é o da célula. É o que o simulador do site conhece para um clique; treino e produto leem a mesma tabela.
- Anúncio fora da cobertura herda a célula coberta mais próxima, até o anel
k = 3 (~0,5 km). A cobertura é a malha r9 recortada pelos polígonos terrestres dos
bairros; um ponto deslocado para a água ou para a margem cai numa célula não
coberta. Quantos foram realocados, e a que distância, está em
_features.json→realocados_por_anel; quem ficasse além de k = 3 ficaria sem feature de localização, contado emsem_celula_coberta— nunca descartado em silêncio. - Cada snapshot recebe a sua safra. 2019 recebe ACS 2015–2019, crimes e 311 de jul/2018–jul/2019 e o aluguel Zillow de jul/2019; 2026 recebe as safras atuais. Features sem safra (metrô, POIs do OpenStreetMap, marcos) são as mesmas nos dois — anacronismo declarado em Fontes externas.
Codificações. Categorias viram inteiros estáveis na ordem do schema.py
(room_type_cod, tipo_imovel_cod, licenca_cod, distrito_cod) — o site envia
exatamente esses códigos. Booleanos viram 0/1 (ausente continua ausente: o LightGBM e
o avaliador JavaScript tratam o ausente pela mesma regra). O preço de 2019 ganha
preco_real, em dólar do snapshot atual pelo CPI-U NY (ADR 0005).