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.
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.
library(jsonlite)
library(RCurl)
library(stringr)
library(pryr)
library(purrr)
library(tidyverse)
library(dplyr)
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.
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
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.
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,
library(doParallel)
library(foreach)
library(doSNOW)
n_cl= detectCores()-1
cl = makeCluster(n_cl)
registerDoParallel(cl)
paste("foram utilizados", n_cl, "cores")
## [1] "foram utilizados 3 cores"
varResp = 'preco.venda'
formula = as.formula(paste(varResp, '~ .'))
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}\]
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))
}
resultado = foreach(i=1:200,.combine = "rbind",.packages = "quantreg") %dopar% {
RQ_holdout_paralel(formula,dados = dados.treino,p=0.7)
}
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 |