Escopo

A computação paralela é uma forma de processamento na qual uma parte dos cálculos são realizados em simultaneamente, aumentando a velocidade de execução de determinados códigos e otimizando o custo computacional. Esse tipo de tácnica opera no princípio de que grandes problemas muitas vezes podem ser divididos em partes menores, que são então resolvidos simultaneamente (“em paralelo”).

Neste case utilizamos os pacotes foreach e doParallel para realizar o processo de validação do modelo preditor em paralelo. Para isso, aplico-os a uma base de amostra de anúncios de imóveis. Nesses anúncios, é possível anunciar para venda, aluguel ou ambos, preenchendo algumas características sobre o imóvel. A base de dados é composta de um arquivo JSON e está disponível no link abaixo:

# https://s3.amazonaws.com/grupozap-data-challenge/data/source-4-ds-train.json.zip

Assim, sabendo que o anúncio é composto por diversas características do imóvel, como tamanho, número de quartos, etc. Neste case, o objetivo será prever uma das principais características do imóvel: seu preço de venda. Dado a possibilidade de existirem diversos tipos de imóveis nos anúncios, estudados somente a previsão acerca dos apartamentos.

1. Trabalhando com Dados em Formato JSON

Um banco de dados JSON (JavaScript Object Notation) é um padrão aberto, humano e legível por máquina, que facilita o intercâmbio de dados e, juntamente com o XML, é o principal formato de intercâmbio de dados usado na web moderna. O JSON suporta todos os tipos de dados básicos que você esperaria: números, seqüências de caracteres e valores booleanos, bem como matrizes e hashes. Basicamente é um conglomerado de listas sob listas que formam um banco de dados relacional.

1.1. Pacotes necessários para pré-processamento

library(jsonlite)
library(RCurl)
library(stringr)
library(pryr)
library(purrr)
library(tidyverse)
library(dplyr)

1.2. Leitura do Banco de Dados em formato JSON

Ao ler o banco de dados, percebe-se que apesar de não ser grande o suficiente para tratamento via sparklyr, não é pequeno o bastante para ser trabalhado de maneira direta.

out = lapply(readLines("source-4-ds-train.json"), fromJSON)
object_size(out)
## 990 MB

Assim, o objetivo foi visualizar amostralmente o banco de dados, tentar retirar o maior número de insigths possíveis a fim de criar uma função que filtrasse somente o necessário para coleta.

1.3. Visualizando o Banco de dados

str(head(out,1))
## List of 1
##  $ :List of 19
##   ..$ usableAreas    : int 388
##   ..$ description    : chr "04 dorms sendo 01 suíte e closet, sala de estar e sala de jantar, cozinha com armários, escritório, 03 w.c, "| __truncated__
##   ..$ title          : chr "PRÓXIMO A AVENIDA PRESIDENTE TANCREDO NEVES"
##   ..$ createdAt      : chr "2017-02-07T13:21:40Z"
##   ..$ publisherId    : chr "f4603b2b52"
##   ..$ unitTypes      : chr "TWO_STORY_HOUSE"
##   ..$ listingStatus  : chr "ACTIVE"
##   ..$ id             : chr "787c7bd19d"
##   ..$ parkingSpaces  : int 6
##   ..$ updatedAt      : chr "2018-12-06T19:27:12.623Z"
##   ..$ owner          : logi FALSE
##   ..$ images         : chr [1:16] "https://s3-sa-east-1.amazonaws.com/vr.images.sp.admin/2752aee7-56a0-4822-b27f-54286c49b14e.jpg" "https://s3-sa-east-1.amazonaws.com/vr.images.sp.admin/f469e1d0-4ff9-42c1-bf23-79ed3c9e3d85.jpg" "https://s3-sa-east-1.amazonaws.com/vr.images.sp.admin/040ce58f-5bb1-40b0-95f0-e428dba9c2be.jpg" "https://s3-sa-east-1.amazonaws.com/vr.images.sp.admin/5e96ba58-d4e0-4b67-acc5-420834f0fdad.jpg" ...
##   ..$ address        :List of 12
##   .. ..$ country     : chr "BR"
##   .. ..$ zipCode     : chr "04290030"
##   .. ..$ city        : chr "São Paulo"
##   .. ..$ streetNumber: chr "53"
##   .. ..$ zone        : chr "Zona Sul"
##   .. ..$ geoLocation :List of 2
##   .. .. ..$ precision: chr "ROOFTOP"
##   .. .. ..$ location :List of 2
##   .. .. .. ..$ lon: num -46.6
##   .. .. .. ..$ lat: num -23.6
##   .. ..$ street      : chr "Rua Juvenal Galeno"
##   .. ..$ locationId  : chr "BR>Sao Paulo>NULL>Sao Paulo>Zona Sul>Jardim da Saude"
##   .. ..$ district    : chr ""
##   .. ..$ unitNumber  : chr ""
##   .. ..$ state       : chr "São Paulo"
##   .. ..$ neighborhood: chr "Jardim da Saúde"
##   ..$ suites         : int 1
##   ..$ publicationType: chr "STANDARD"
##   ..$ bathrooms      : int 3
##   ..$ totalAreas     : int 388
##   ..$ bedrooms       : int 4
##   ..$ pricingInfos   :List of 2
##   .. ..$ price       : int 700000
##   .. ..$ businessType: chr "SALE"

Ao todo, o banco de dados é composto por 133964 listas, sendo cada uma delas com outros mais dois subníveis de listas. Essas listas são compostas conforme saída acima e podem ser interpretadas da seguinte maneira:

usableAreas: tamanho do imóvel em m²

description: descrição do anúncio

title: título do anúncio

createdAt: timestamp da criação do anúncio

publisherId: identificador do anunciante

unitTypes: tipo do imóvel (apartamento, casa, comércial, etc)

listingStatus: status do anúncio (ativo ou inativo)

id: identificador único do anúncio

parkingSpaces: número de vagas de garagem

updatedAt: timestamp do último update

owner: indicador se o anunciante é proprietário do imóvel

images: lista de links de imagens do anúncio

address: endereço do imóvel

country: país do imóvel

zipCode: CEP do imóvel

city: cidade do imóvel

streetNumber: número da rua

zone: zona do imóvel

geoLocation: geolocalização do imóvel

street: nome da rua

locationId: id da localização

district: distrito do imóvel

unitNumber: complemento do imóvel

state: estado federativo do imóvel

neighborhood: bairro do imóvel

publicationType: tipo de publicação do anúncio (normal ou premium)

bathrooms: número de banheiros

totalAreas: área total do imóvel em m²

bedrooms: número de quartos

suites: número de suites

princingInfos: informações do preço

price: preço de venda

yearlyIptu: preço do IPTU

businessType: tipo do anúncio (venda, aluguel ou ambos)

monthlyCondoFee: condomínio

rentalTotalPrice: preço do aluguel

1.4. Tratamento do Banco de Dados

Após análise visual do banco de dados, algumas variáveis foram inicialmente descartadas:

geoLocation, locationId = Só usaria se fosse necessário fazer alguma analise em plataforma de BI com o banco de dados.

listingStatus = Não identifiquei explicabilidade em relação ao preço do imóvel.

id = Partindo da priori que cada anuncio poderia ter um identificador específico, não faria sentido utilizar.

street, district e unitNumber = Achei que esse nível de precisão não agregaria. Afinal, índices que estão diretamente ligados aos valores dos imóveis, como por exemplo, INCC e CUB, tomam o bairro como georeferência. Além do mais, a não ser que houvesse uma repetição considerável dos bairros no banco de dados, utilizar essa variável poderia descaracterizar a idéia de fator (vide que poderia não se repetir nos dados de teste).

Além das exclusões, analisando a descrição de alguns anúncios, por amostragem aleatória, verifiquei o interesse em informar a respeito da existência de piscina e churrasqueira da maioria deles. Desta forma, entendi que ambos poderiam ser insumos consideráveis na previsão do preço dos imóveis. Assim, criei as variáveis: piscina = identifica a existência de piscina no anúncio sobre o imóvel churrasqueira = identifica a existência de churrasqueira no anúncio sobre o imóvel.

Outra avaliação foi referente as variáveis usableAreas e totalAreas, que além de muitos dados faltantes em uma das duas, no caso de alguns tipos de imóveis, como apartamentos por exeplo, deveriam apresentar a mesma informação, por vezes traziam valores distintos: ora a área interna trazia valores coerentes, ora a área total que trazia valores coerentes. O que me trouxe evidências que a imputação dessas informações nos anúncios, pode ter sido falha. Desta forma, criei a variável area substituindo as duas, e trazendo o valor real do imóvel anunciado.

Por fim, duas informações dos anúncios foram adaptadas em duas novas variáveis. As informações createdAt e updatedAt, referêntes às datas de criação e atualização dos anúncios, respectivamente, foram substituídas por:

ano.referencia = Ano que o anúncio sofreu última alteração (caso não tenha sofrido alteração, considerei a data de criação)

periodo.anuncio = Trás, em meses, o tempo que o anúncio ficou no site (no caso, a diferença entre as datas de criação e atualização). Isto porque, implicitamente, imagina-se que um imóvel anunciado por longo período, não foi vendido. E assim, tende a sofrer reajuste negativo em seu preço. Supondo que o ano de referência é o ano o qual foi retirado o anúncio, quando, não existe “updatedAt” o “periodo.anuncio” é igual a zero.

1.5. Função de Coleta por Filtros:

Aqui, dado a possibilidade de coletar diferentes estratos do banco de dados original, a idéia foi criar uma função que já apresentasse o banco de dados estratificado, pelos tipo de imóvel e tipo de demanda que o usuário tenha interesse em estudar.

tratamento.dos.dados = function(referencia = out,
FILTRO1 = "SALE", FILTRO2= "APARTMENT"){
library(rlist)
library(RCurl)
library(stringr)
dados = data.frame(aux= 1:length(out))
id.estrato.1= function(out){
tipo.venda = character()
for (i in 1:length(out)) {
tipo.venda[i]= out[[i]][['pricingInfos']][['businessType']]
}
return(tipo.venda)
}
dados$tipo.venda = id.estrato.1(out)
id.estrato.2= function(out){
tipo = character()
for (i in 1:length(out)) {
tipo[i]= out[[i]][['unitTypes']]
}
return(tipo)
}
dados$tipo = id.estrato.2(out)

id.piscina = function(out){
piscina = numeric()
for (i in 1:length(out)) {
piscina[i]= sum(strsplit(str_remove_all(out[[i]][['description']],
"[,.]"), " ")[[1]]=="piscina")

piscina[i] = ifelse(piscina[i]==0,"0","1")
}
return(piscina)
}
dados$piscina= id.piscina(out)
dados$piscina= as.factor(dados$piscina)
id.churrasqueira = function(out){
churrasqueira = numeric()
for (i in 1:length(out)) {
churrasqueira[i]= sum(strsplit(str_remove_all(out[[i]][['description']],

"[,.]"), " ")[[1]]=="churrasqueira")
churrasqueira[i] = ifelse(churrasqueira[i]==0,"0","1")
}
return(churrasqueira)
}
dados$churrasqueira= id.churrasqueira(out)
dados$churrasqueira= as.factor(dados$churrasqueira)

id.tempo = function(out){
periodo.anuncio = numeric()
for (i in 1:length(out)) {
periodo.anuncio[i]= ifelse(is.null(out[[i]][['updatedAt']]), 0,

12*(abs(as.numeric(strsplit(out[[i]][['updatedAt']], "-")[[1]][1]) -
as.numeric(strsplit(out[[i]][['createdAt']], "-")[[1]][1]))) +
(as.numeric(strsplit(out[[i]][['updatedAt']], "-")[[1]][2]) -
as.numeric(strsplit(out[[i]][['createdAt']], "-")[[1]][2])))

}
return(periodo.anuncio)
}
dados$periodo.anuncio = id.tempo(out)

id.anunciante = function(out){
tipo.anunciante = character()
for (i in 1:length(out)){
tipo.anunciante[i]= out[[i]][['owner']]
}
return(tipo.anunciante)
}
dados$tipo.anunciante = id.anunciante(out)
dados$tipo.anunciante = as.factor(dados$tipo.anunciante)
id.imagens = function(out){
n.imagens = numeric()
for (i in 1:length(out)) {
n.imagens[i]= length(out[[i]][['images']])
}
return(n.imagens)
}
dados$n.imagens = id.imagens(out)

id.anuncio = function(out){
tipo.anuncio = character()
for (i in 1:length(out)) {
tipo.anuncio[i]= out[[i]][['publicationType']]
}
return(tipo.anuncio)
}
dados$tipo.anuncio = id.anuncio(out)
dados$tipo.anuncio = as.factor(dados$tipo.anuncio)
id.IPTU = function(out){
valor.anual.IPTU= numeric()
for (i in 1:length(out)) {
valor.anual.IPTU[i] = ifelse(is.null(out[[i]][['pricingInfos']][['yearlyIptu']]) ,0,

out[[i]][['pricingInfos']][['yearlyIptu']])
}
return(valor.anual.IPTU)
}
dados$valor.anual.IPTU = id.IPTU(out)

id.condominio = function(out){
valor.mensal.cond = numeric()
for (i in 1:length(out)) {
valor.mensal.cond[i]= ifelse(is.null(out[[i]][['pricingInfos']][['monthlyCondoFee']]) ,0,
out[[i]][['pricingInfos']][['monthlyCondoFee']])
}
return(valor.mensal.cond)
}
dados$valor.mensal.cond = id.condominio(out)

id.PRECO= function(out){
preco.venda = numeric()
for (i in 1:length(out)) {
preco.venda[i]= out[[i]][['pricingInfos']][['price']]
}
return(preco.venda)
}
dados$preco.venda = id.PRECO(out)

id.vagas = function(out){
n.vagas= numeric()
for (i in 1:length(out)) {
n.vagas[i]= ifelse(is.null(out[[i]][['parkingSpaces']]) ,0,out[[i]][['parkingSpaces']])
n.vagas[i] = ifelse(n.vagas[i]>=11,NA,n.vagas[i])
}
return(n.vagas)
}
dados$n.vagas = id.vagas(out)

id.banheiros = function(out){
n.banheiros= numeric()
for (i in 1:length(out)) {
n.banheiros[i]= ifelse(is.null(out[[i]][['bathrooms']]) ,0,out[[i]][['bathrooms']])
n.banheiros[i] = ifelse(n.banheiros[i]>=12,NA,n.banheiros[i])
}
return(n.banheiros)
}
dados$n.banheiros = id.banheiros(out)

id.quartos = function(out){
n.quartos= numeric()
for (i in 1:length(out)) {
n.quartos[i]= ifelse(is.null(out[[i]][['bedrooms']]) ,0,out[[i]][['bedrooms']])
n.quartos[i] = ifelse(n.quartos[i]>=12,NA,n.quartos[i])
}
return(n.quartos)
}
dados$n.quartos = id.quartos(out)

id.suites = function(out){
n.suites = numeric()
for (i in 1:length(out)) {
n.suites[i] = ifelse(is.null(out[[i]][['suites']]) ,0,out[[i]][['suites']])
n.suites[i] = ifelse(n.suites[i]>=12,NA,n.suites[i])
}
return(n.suites)
}
dados$n.suites = id.suites(out)

id.zonas = function(out){
id.zona = character()
for (i in 1:length(out)) {
id.zona[i]= ifelse(out[[i]][['address']][['zone']]=="",

strsplit(out[[i]][['address']][['locationId']], ">")[[1]][5],
out[[i]][['address']][['zone']])

id.zona[i] =ifelse(is.na(id.zona[i]),"Barrios",id.zona[i])
}
return(id.zona)
}
dados$id.zona = id.zonas(out)
dados$id.zona = as.factor(dados$id.zona)

id.area.do.ap = function(out){
area.real = numeric()
area.interna= numeric()
area.total=numeric()
for (i in 1:length(out)) {
area.interna[i]= ifelse(is.null(out[[i]][['usableAreas']]),NA,out[[i]][['usableAreas']])
area.total[i]= ifelse(is.null(out[[i]][['totalAreas']]),NA,out[[i]][['totalAreas']])
area.real[i]= ifelse((is.na(area.total[i]) | area.total[i]==0 |

area.total[i] > 1000 | area.total[i] < 30),
area.interna[i],area.total[i])
area.real[i]= ifelse(area.real[i]<=10,NA,area.real[i])
area.real[i]= ifelse(area.real[i]>=1000,NA,area.real[i])
}
return(area.real)
}
dados$area = id.area.do.ap(out)

formula1 = c(paste(FILTRO1))
formula2 = c(paste(FILTRO2))
dados = dados[which(dados$tipo.venda == formula1),] # BANCO DE DADOS DE ANUNCIO DE VENDAS
dados = dados[which(dados$tipo == formula2),]
dados$tipo = NULL
dados$tipo.venda = NULL
dados$aux = NULL
dados= dados[complete.cases(dados),]
return(dados)
}

Nese Case, o estudo foi realizado acerca dos apartamento a venda. Desta forma, a função foi aplicada da seguinte maneira

library(rlist)
dados.treino=tratamento.dos.dados(referencia = out,FILTRO1="SALE",FILTRO2="APARTMENT")
object.size(dados.treino)
## 6622000 bytes

Contudo, observe que o banco equivale a aproximadamente metade do banco original, o que ainda pode ser considerado um banco pesado, e de lento processamento no formato convencional do R.

Desta forma, a alternativa para tratamento desses dados foi usar o paralelismo para etapa de validação do modelo, ou seja,

2. Modelagem via Validação Paralelizada

2.1. Importação de Pacotes Necessários

library(doParallel)
library(foreach)
library(doSNOW)   

2.2. Importação de Pacotes Necessários

n_cl= detectCores()-1
cl =  makeCluster(n_cl)
registerDoParallel(cl)
paste("foram utilizados", n_cl, "cores")
## [1] "foram utilizados 3 cores"

2.3. Definição da Fórmula do Modelo e da Variável Resposta

varResp = 'preco.venda'
formula = as.formula(paste(varResp, '~ .')) 

2.4. Modelagem via Regressão Quantílica por Holdout repetido 200 vezes com desempenho medido pelas Proporções da Estatística de Theil (1958)

A ideia de usar a regressão quantílica foi porque pelo volume dos dados, a análise exploratória foi feita de maneira amostral, o que pode omitir certos outliers que prejudiquem a modelagem. Desta forma, a regressão quantílica faz todo o sentido. A principal vantagem da metodologia de regressão quantílica é que o método permite entender relacionamentos entre variáveis fora da média dos dados, tornando-o útil para entender resultados que não são normalmente distribuídos e que têm relacionamentos não lineares com variáveis preditoras. Além disso, a mediana é uma medida robusta de tendência central, isto porque enquanto a mediana tem um ponto de ruptura de 50%, a média tem um ponto de ruptura de 1/n, onde n é o número de pontos de dados.

Assim, prever o valor mediano dos Preço dos imóveis pode garantir um bom desempenho para dados, ainda que tenham sido extraídos de uma ampla variedade de distribuições de probabilidade, especialmente para distribuições que não são normais.

Quanto a medir o desempenho do modelo pelas Proporções da Estatística de Theil (1958). Deve-se saber que a Estatística proposta por Theil (1958) é uma derivação do estudo sobre o EQM de fácil interpretabilidade. O que torna a comunicação entre corpo técnico e usuários com pouco expertise na área de ciência de dados muito mais facilitada. Além disso, Ahlburg, D. (1984), Pindyck, R. e Rubinfeld, D. (1997) e Makridakis, S.,Wheelwright, S. e Hyndman, R. (1998), tratam da utilização das Proporções da Estatística de Theil para mensurar performance de modelos em situações reais. Isto porque, por construção, as parcelas \(U_m\), \(U_r\) e \(U_c\) são complementares e somadas resultam em 1. Em que, quanto maior a parcela \(U_c\), melhor a performance do modelo, e consequentemente, menores as parcelas do viés (\(U_m\)) e da varibilidade (\(U_r\)) dos resultados preditos. As equações abaixo representam matematicamente essas proporções:

\[ U_m = \frac{(\bar{Y}- \bar{\hat{Y}})^2}{\frac{1}{n}\sum_{i=1}^n \left[ Y_i-\hat{Y}_i \right]^2} \ \ ; \ \ U_r = \frac{(\sigma_Y - \sigma_{\hat{Y}})^2}{\frac{1}{n}\sum_{i=1}^n \left[ Y_i-\hat{Y}_i \right]^2} \ \ ; \ \ U_c = \frac{2(1-r)\sigma_Y\sigma_{\hat{Y}}}{ \frac{1}{n}\sum_{i=1}^n \left[ Y_i-\hat{Y}_i \right]^2}\]

2.4.1 Função da Regressão Quantílica Holdout

RQ_holdout_paralel= function(formula,dados,p=0.7){
  ind=sample(x = 1:nrow(dados), size = round(p*nrow(dados)))
  treino=dados[ind,] 
  teste=dados[-ind,]    
  cls =  rq(formula = formula, tau = 0.5, data = data.frame(treino))
  pred = predict(object=cls, newdata = data.frame(teste))
  ind2 = which(colnames(teste) == as.character(formula)[2])
  y = teste[, ind2]
  estimados.medios= mean(pred)
  observados.medios= mean(y)
  estimados.desvios = sd(pred)
  observados.desvios = sd(y)
  r = cor(pred,y)
  residuos = pred-y
  EQM = mean((residuos)^2)
  numerador.Var = (estimados.medios - observados.medios)^2
  numerador.Vies = (estimados.desvios - observados.desvios)^2
  numerador.cov = 2*(1-r)*estimados.desvios*observados.desvios
  Um = numerador.Vies/ EQM
  Ur = numerador.Var/ EQM
  Uc = numerador.cov/EQM
  return(list(Um = Um,Ur = Ur,Uc = Uc))
}

2.4.2 Função de Holdout Repetidas vezes via paralelismo

resultado = foreach(i=1:200,.combine = "rbind",.packages = "quantreg") %dopar% {
  RQ_holdout_paralel(formula,dados = dados.treino,p=0.7)
}

2.4.3 Desempenho do Modelo

Apesar de repetir o processo de validação 200 vezes, ainda é esperado um grau elevado da variabilidade do desempenho. Isto dado às características dos dados, bem como pela técnica de data splitting utilizada (sabe que o Holdout apresenta alta variabilidade de resultados).

Portanto, além da estatística pontual (a média), é valido uma avaliação gráfica que possibilite enxergar o comportamento da performance do modelo ao longo das repetições Holdout. Para isso, gerei o boxplot das proporções encontradas no processo.

library(knitr)
matrix.de.resultados = matrix(NA,nrow =200, ncol = 3 )
for (i in 1:nrow(resultado)) {
matrix.de.resultados[i,]= c(resultado[i,][[1]], resultado[i,][[2]],resultado[i,][[3]])
}
colnames(matrix.de.resultados) = c("Um", "Ur", "Uc")
boxplot(matrix.de.resultados, col = c("tomato", "lightblue","orange"), main = 'Desempenhos Holdout Repetidos 200 Vezes')

medidas = c("Um", "Ur", "Uc")
resultados.medios = round(apply(matrix.de.resultados, 2, mean),3)
kable(rbind(medidas,resultados.medios))
Um Ur Uc
medidas Um Ur Uc
resultados.medios 0.342 0.007 0.651