perjantai 25. lokakuuta 2019

Anomaly detection R:llä Part 1.

Anomaly detection:lla tarkoitetaan usein koneoppimisen "(Semi) Unsupervised"-menetelmiä, joilla datasta voidaan havaita mahdolliset poikkeamat ts. harvinaiset havainnot. Esimerkiksi, jos henkilö X käyttää poikkeuksellisesti koko luottolimiittinsä kerralla, niin tämä transaktio erottuu muista huomattavasti. Miten pankki havaitsee tällaiset poikkeavat tapaukset? Yksinkertaisessa tapauksessa pankki voi tarkastella henkilön X luottotilitapahtumia ja päättää transaktioille suuruus- ja määrärajat, jotka hälyttävät, kun rajat ylitetään.

Esimerkki 1. Yksi normaalijakautunut muuttuja.

Henkilöllä X on 1000 transaktiota viimeisen vuoden aikana ja pankki haluaa asettaa hälytysrajat, jotta epäilyttävän suuret transaktiot voidaan tarkistaa manuaalisesti. Tehdään transaktioista histogrammi ja nähdään, että transaktiot ovat suunnilleen normaalisti jakautuneet. Normaalijakauma-oletuksen avulla tiedetään, että n. 0.15% tapahtumista asettuu plus kolmen keskihajonnan päähän keskiarvosta. Esimerkissä Cut-off-piste asettuu kohtaan 137 €, eli kaikki tämän ylittävät transaktiot hälyttäisivät.

a <- data.frame(transaktiot=c(rnorm(995, mean=70,sd=20),130,140,150))
a <-  -min(a$transaktiot)+a
#Valitaan cut-off-piste kolmen keskihajonnan päästä keskiarvosta.
cutoff <- mean(a$transaktiot)+3*sd(a$transaktiot)

ggplot(data=a,aes(x=transaktiot))+
  geom_histogram(binwidth=5, fill="blue") +
  labs(title="1000 tilitapahtumaa (Cut-off-piste asetettu kolmen keskihajonnan päähän keskiarvosta)",x="Transaktion suuruus €",y="Määrä")+
  theme(plot.title = element_text(hjust = 0.5))+
  geom_vline(xintercept = cutoff, linetype="dotted", 
             color = "red", size=1.5)+
  geom_segment(aes(x = 118, y = 76, xend = 130, yend = 73),size=1.5,
               arrow = arrow(length = unit(0.5, "cm")))+
   annotate(geom="text", x=113, y=79, label="Cut-off-piste",
             color="red",size=6)




Esimerkki 2. Multivariate Gaussian: Useampi riippumaton normaalijakautunut muuttuja.

Normaalijakauma-oletuksen avulla voitaisiin lisätä mukaan myös muita muuttuja (esim. transaktion kellon aika) ja laskea eri yhdistelmille todennäköisyydet. Tällöin hälytys voitaisiin asettaa esim. samaan kohtaan "0.15%", eli vain tämän todennäköisyyden alittavat tapahtumat hälyttäisivät. Käytännössä laskenta tapahtuu niin, että estimoimme kaikille muuttujille keskiarvot sekä keskihajonnat ja saamme näin jokaiselle muuttujalle estimoitua tiheysfunktion (Probability density function). Tämän jälkeen voimme laskea jokaiselle muuttujan arvolle todennäköisyyden. Otetaan esimerkiksi tilanne, kun meillä on kaksi muuttujaa X1 ja X2 (Huom. teemme oletuksen muuttujien riippumattomuudesta).

X1:n keskiarvo on 5 ja keskihajonta 3.
X2:n keskiarvo on 2 ja keskihajonta 1.

Nyt voimme ratkaista, kuinka todennäköistä on saada näistä jakaumista esim. arvot X1=1 ja X2=4.

pX1 <- dnorm(1,mean=5,sd=3)
pX2 <- dnorm(4,mean=2,sd=1)
pX1*pX2
=0.29%

Näemme, että jos cut-off-pisteemme on 0.15% (0.29%>0.15%), niin kyseessä ei ole poikkeava havainto.

Katsotaan esimerkkiä vielä luomalla kuva, jossa on alkuperäisten datapisteiden lisäksi myös Contour-kuva, joka näyttää kohdat (isoviivat), joissa todennäköisyydet ovat samat. Esimerkiksi keskimmäinen isoviiva näyttää alueen, jossa todennäköisyys on korkein. Näemmekin, että tällä alueella todennäköisyydet ovat 4%:n tienoilla.

#Tehdään kaksi muuttujaa 
X1 <- c(rnorm(20,mean=5,sd=3),11,15,16)
X2 <- c(rnorm(20,mean=2,sd=1),5,3,8)
data <- data.frame(X1,X2)
#Lasketaan todennäköisyydet eri yhdistelmille
data$probability <-  dnorm(data$X1,mean=5,sd=3)*dnorm(data$X2,mean=2,sd=1)*100
#Luokitellaan alle 0.15% arvon saavat outliereiksi
data$class_ <- ifelse(data$probability < 0.15,"Outlier","Normaali")
#interpoloidaan arvoja Contour-kuvaa varten
ipdata <- with(data, interp(x=X1, y=X2, z=probability))

ipdata2 <- expand.grid(x=ipdata$x, y=ipdata$y)

ipdata2$z <- as.vector(ipdata$z)
head(ipdata2)
#https://stackoverflow.com/questions/19065290/r-ggplot-stat-contour-not-able-to-generate-contour-lines
head(ipdata2)
ggplot(data, aes(x=X1, y=X2))+
   stat_contour(data=na.omit(ipdata2), binwidth=1, colour="red", aes(x=x, y=y, z=z))+
   geom_point(aes(color=class_),size=3)+
  geom_text(aes(label=paste(round(probability,2),"%")),hjust=0, vjust=1.5)+
  labs(title="Normaalijakautuneet muuttujat ja Anomaly detection (Cut-off-piste 0.15%)",color="Luokittelu")+
  theme(plot.title = element_text(hjust = 0.5),legend.title=element_text(size=15),legend.text=element_text(size=12))






Esimerkki 3.  Multivariate Gaussian: Useampi keskenään korreloitunut muuttuja.

Mitä jos muuttujat korreloivat keskenään? Yllä oletettiin, että muuttujat ovat toisistaan täysin riippumattomia. Todellisuudessa esim. transaktion suuruus ja kellon aika voivat korreloida (esim. yöajan ostokset eroavat luultavasti iltapäivän ostoksista). Ottamalla korrelaation pystymme mallintamaan dataa generoivaa prosessia paremmin, kun muuttujat korreloivat keskenään.

Luodaan data, jossa muuttujat korreloivat keskenään ja lasketaan eri malleilla todennäköisyydet tapahtumille. Lasketaan todennäköisyydet ensin riippumattomuus-oletuksen mallille.

set.seed(1)
X1 <- c(rnorm(20,mean=5,sd=3),11,15,16)
X2 <- c(rnorm(20,mean=7,sd=1),8,9,10)
cor(X1,X2)

data <- data.frame(X1,X2)



#Lasketaan todennäköisyydet eri yhdistelmille
data$probability <-  dnorm(data$X1,mean=mean(X1),sd=sd(X1))*dnorm(data$X2,mean=mean(X2),sd=sd(X2))*100
#Luokitellaan alle 0.15% arvon saavat outliereiksi
data$class_ <- ifelse(data$probability < 0.15,"Outlier","Normaali")
#interpoloidaan arvoja Contour-kuvaa varten
ipdata <- with(data, interp(x=X1, y=X2, z=probability))

ipdata2 <- expand.grid(x=ipdata$x, y=ipdata$y)

ipdata2$z <- as.vector(ipdata$z)

#Luodaan "Riippumattomuus-oletuksella"-kuva
plot1 <- ggplot(data, aes(x=X1, y=X2))+
  stat_contour(data=na.omit(ipdata2), binwidth=1, colour="red", aes(x=x, y=y, z=z))+
  geom_point(aes(color=class_),size=3)+
  geom_smooth(method="lm",se=F)+
  geom_text(aes(label=paste(round(probability,2),"%")),hjust=0, vjust=1.5)+
  labs(title="Riippumattomuus-oletuksella",color="Luokittelu")+
  theme(legend.position = "none",plot.title = element_text(hjust = 0.5),legend.title=element_text(size=15),legend.text=element_text(size=12))


Lasketaan ennusteet seuraavaksi ottamalla muuttujien välinen korrelaatio huomioon estimoidussa tiheysfunktiossa.

data2 <- data[,1:2]
centered <- as.matrix(data.frame(lapply(data2[,1:2],function(x) x-mean(x))))
sigma2=(var(Xval_centered))


#Lasketaan estimoidusta tiheysfunktiosta todennäköisyydet
library(MASS)
a=(2*pi)^(-ncol(Xval_centered)/2)*det(sigma2)^(-0.5)

b= exp(-0.5 *rowSums((Xval_centered%*%ginv(sigma2))*Xval_centered))
prediction=a*b

data2$probability <- prediction*100
data2$class_ <- ifelse(data2$probability <0.15,"Outlier","Normaali")

ipdataz <- with(data2, interp(x=X1, y=X2, z=probability))

ipdata2z <- expand.grid(x=ipdataz$x, y=ipdataz$y)

ipdata2z$z <- as.vector(ipdataz$z)

#Tehdään kuva
plot2 <- ggplot(data2, aes(x=X1, y=X2))+
  stat_contour(data=na.omit(ipdata2z), binwidth=1, colour="red", aes(x=x, y=y, z=z))+
  geom_point(aes(color=class_),size=3)+
  geom_smooth(method="lm",se=F)+
  geom_text(aes(label=paste(round(probability,2),"%")),hjust=0, vjust=1.5)+
  labs(title="Korrelaatio huomioitu",color="Luokittelu")+
  theme(plot.title = element_text(hjust = 0.5),legend.title=element_text(size=15),legend.text=element_text(size=12))+
geom_segment(aes(x = 14, y = 9.5, xend = 14.7, yend = 9.1),size=1.5,
             arrow = arrow(length = unit(0.5, "cm")))+
 annotate(geom="text", x=12.1, y=9.6, label="Todennäköisyys kasvanut",
                       color="red",size=4)

library(gridExtra)

grid.arrange(plot1, plot2, ncol=2,widths=c(2,2.5))

Alla olevasta kuvasta (vasen) näemme aiemmin esitellyn mallin, jossa oletettiin, että muuttujat ovat toisistaan riippumattomia. Oikealla on samalle datalle sovitetun korrelaation huomioivan tiheysfunktion arvot. Sininen viiva on regressioviiva, joka näyttää muuttujien välisen positiivisen korrelaation (r=0.41). Näemme kuinka oikeassa reunassa oleva piste muuttuukin oulierista normaaliksi korrelaation huomioimisen johdosta. Tämä johtuu siitä, että vaikka piste on kaukana massasta, niin silti se on regressiosuoran suuntainen. Näemme myös, että oikean puoleisessa kuvassa alareunan arvot eivät saa enää niin suuria todennäköisyyksiä. 







Havainnollistetaan kahden mallin eroja vielä simuloimalla kaksi voimakkaasti korreloivaa muuttujaa (r=-0.71). Oikean puoleisessa kuvassa otetaan korrelaatio huomioon ja näemme, miten isoviivat ovat lähes kiinni kiinni havannoissa. Vasemman puolimmaisessa kuvassa korrelaatiota ei oteta huomioon, joten isoviivat muodstavat pyöreän alueen havaintojen keskustan päälle.






Alla vielä kuva oikenpuoleisesta tilanteesta. Näemme, että kun keskustasta lähdetään kasvattamaan X1:stä ja samalla pienentämään X2:sta, niin pysymme pidempään korkeimmalla todennäköisyys-alueella. Tämä johtuu korrelaation huomioimisesta. Jos emme huomioi korrelaatiota, niin vastaavassa tilanteessa todennäköisyys laskee jyrkemmin.








Kumpi malleista on parempi?

Vaikka korrelaation huomioiva malli voi kuvastaa paremmin dataa generoivaa prosessia, niin oleellista on miettiä, mikä "outlier" on tilanteessa. On siis tärkeää, että pystymme mittaamaan mallien toimivuutta käytännössä, jotta emme jää vain teoreettiselle pohjalle. Käytännössä voimme määritellä outlierit, kuten tunnetut luottotilin petostapahtumat ja laskea todennäköisyydet näille tapahtumille. Voimme esim. tehdä mallinnuksen täysin puhtaalla datalla, jossa ei ole outliereita ja sen jälkeen sovittaa mallin dataan, jossa on seassa outliereita. Tästä datasetistä voimme tarkastella performanssia ja valita sopivan cut-off-pisteen, jolla outlierit saada tehokkaimmin kiinni. Lopuksi voimme kokeilla mallia täysin uuteen dataan ja katsoa, miten se performoi. Tärkeää cut-off-pistettä on käyttää Precision-Recall-metriikoita, jotta huomioimme myös kustannukset, jotka syntyvät väärin outliereiksi luokitelluista tapahtumista. 

Tässä esitellyt anomaly detection -mallit soveltuvat datalle, jotka ovat normaalijakautuneet. Vaikka alkuperäinen data ei olisikaan normaalijakautunut, niin transformaatiot voivat auttaa (esim. log). Jos data ei ole normaalijakautunutta, niin kannattaa kokeille tekniikoita, joissa normaalijakauma-oletusta ei ole mukana. Kirjoittelen näistä tekniikoista jatkossa lisää. 


tiistai 15. lokakuuta 2019

Shiny - Rstudio: Kohderyhmätyökalu

Mikäli analyytikolle sataa usein rutiininomaisia pieniä kohderyhmäpyyntöjä, kannattaa pyrkiä ratkaisuun. Pienistä puroista kasvaa helposti suuria ja analyytikkoja kuormittavia työtaakkoja.

Ajatellaan esim. tilannetta, jossa osasto X pyytää usein listoja, koska listalle kuuluvat yritykset vaihtuvat tiuhaan tahtiin. Analyytikolle tämä tarkoittaa SQL-kyselyn muokkaamista, ajamista sekä valmiin tiedoston lähettämistä. Aikaa tähän kaikkeen ei välttämättä mene kuin 15-30min, mutta yllättävä aikasyöppö voi olla epätäydellinen informaatio. Ymmärsikö analyytikko tarpeet oikein? Vai unohtiko listan pyytäjä sanoa jotain ja täten lista pitää tehdä uusiksi? Tällaisen pienen työn tekeminen voi lopulta viedä paljon enemmän aikaa, ja kun tätä tapahtuu usein, niin kuukaudessa puhutaan jo huomattavista ajallisista kustannuksista.

Mikä avuksi?

1. Sovitaan tietyt listat automatisoitaviksi esim. kuukausittain. Tällöin aikaa säästyy, mutta toisaalta lista pysyy samana, eli siihen ei pysty tekemään muokkauksia ilman analyytikon välikättä.

2. Parannetaan informaation kulkua, jotta ei tule väärinkäsityksiä. Keskustellaan steikkareiden kanssa rohkeasti, jotta jatkossa listan pyytäjä näkee vain valittavat pizzatäytteet, eli tehdään valinnasta helppoa.

3. Mietitään voisiko listan toteuttaa niin, että listan pyytäjä voisi tehdä muokkaukset ja listan haut itse. Tässä kohtaa RStudion Shiny voi olla suureksi avuksi.

Mikä on Shiny?

Shiny on R-paketti, jolla voi tehdä interaktiivisia web-applikaatioita suoraan R:n kautta. Käytännössä loppukäyttäjä näkee nettisivun, jossa hän voi valita esim. tietyn tuotteen myynnit haluamilleen vuosille. Shiny mahdollistaa siis samoja asioita kuin PowerBi ja Qlik Sense, eli helpottaa tiedon hakua. Shinyn etu edellä mainittuihin työkaluihin on, että sillä voi tehdä kaikkea mitä ärrässäkin voi, eli se mahdollistaa mm. tehokkaammat visualisoinnit.

Shiny apuna listojen teossa

Shinylla pystyy tekemään näkymän, jossa loppukäyttäjä voi valita esim. kaikki asiakkaat, jotka ovat ostaneet tuotetta X. SQL-muodossa tämä tarkoittaa, että loppukäyttäjä pystyy valitsemaan tuotteet, jotka analyytikko muuten syöttäisi manuaalisesti "Where"-lausekkeeseen. Shinylla loppukäyttäjä pystyy siis syöttämään haluamansa kriteerit SQL-lausekkeeseen selkeän käyttöliittymän avulla.

Alla näkymä tekemästäni esimerkkiäpistä. Käyttäjän kaikki valinnat ovat yhteydessä SQL-kyselyyn. Alla olevalla työkalulla käyttäjä pystyy valitsemaan yrityslistan (esim. kohderyhmät) toimialan ja toteutuneiden myyntien perusteella. Lopuksi käyttäjä voi lähettää valmiin listan excelinä.



Miten tällainen työkalu tehdään?

Luodaan ensin tietokanta MySQL:ssä.


create database datamyynnit;
use datamyynnit;
create table asiakkuudet (asiakasID int(10) unsigned auto_increment primary key not null, 
                          Kaupunki varchar(30) not null, Puhelinnumero varchar(20) not null, Toimiala varchar(10) not null);

create table myynnit(asiakasID int(10) not NULL, tilausnumero int(10) unsigned primary key not null, 
                      maara int(10) not null, tuote varchar(30) not null,hinta int(10) not null, pvm date not null);


insert into asiakkuudet (Kaupunki,Puhelinnumero,Toimiala)  values("Espoo","034343434","A");
insert into asiakkuudet (Kaupunki,Puhelinnumero,Toimiala)  values("Helsinki","032004332","A");
insert into asiakkuudet (Kaupunki, Puhelinnumero,Toimiala) values("Espoo","034363555","B");
insert into asiakkuudet (Kaupunki, Puhelinnumero,Toimiala) values("Helsinki","035545555","B");

insert into myynnit (asiakasID,tilausnumero,maara,tuote,hinta,pvm) values("2","15","1","tuote1","900","2019-09-10");
insert into myynnit (asiakasID,tilausnumero,maara,tuote,hinta,pvm) values("1","16","1","tuote2","800","2019-09-15");
insert into myynnit (asiakasID,tilausnumero,maara,tuote,hinta,pvm) values("1","17","1","tuote1","900","2019-09-15");
insert into myynnit (asiakasID,tilausnumero,maara,tuote,hinta,pvm) values("3","18","1","tuote2","800","2019-09-28");
insert into myynnit (asiakasID,tilausnumero,maara,tuote,hinta,pvm) values("3","19","1","tuote1","900","2019-09-26");
insert into myynnit (asiakasID,tilausnumero,maara,tuote,hinta,pvm) values("4","20","1","tuote1","900","2019-09-25");

Nyt kun meillä on tietokanta, voimme tehdä kyselyjä R:n kautta.
Esim.
sqltk = dbConnect(MySQL(), user='root', password='1234', dbname='datamyynnit', host='localhost')
dbGetQuery(sqltk,"select * from myynnit")

Seuraavaksi on luotava Shinylla UI:

library(shiny)
library(dbConnect)
library(RMySQL)
# Define UI for application that draws a histogram
shinyUI(fluidPage(
    
    
    titlePanel("Kohderyhmätyökalu"),
    
   
    sidebarLayout(
        sidebarPanel(
            helpText("Valitse haluamasi kohderyhmä valikoista ja klikkaa lopuksi 'Printtaa excelinä'-painiketta."),
            
            selectInput("tol",
                        label=("Toimiala"),
                        choices = c("Voit valita useita"='',c("TOL A" ="A",
                                                              "TOL B"="B")),
                                    multiple=T, selected=c("A","B")
                        ),
            
            selectInput("tuote",
                        label=("Myynnit tuotteille"),
                        choices = c("Voit valita useita"='',c("Tuote 1" ="tuote1",
                                                              "Tuote 2"="tuote2")),
                                    multiple=T,selected=c("Tuote 1"="tuote1",
                                                          "Tuote2"="tuote2")),
            
            dateRangeInput("dates", 
                           label = ("Myyntien aikaväli"),
                           start = Sys.Date() - 90, end = Sys.Date() - 1,
                           format = "yyyy-mm-dd",
                           language = "fi",
                           separator = "-"
                           
            ),
            #minimimyynti
            sliderInput("minmyynti", "Minimimyynti:",
                        min = 0, max = 10000, value = 0
            )
                        ,
            actionButton("printti", "Printtaa excelinä")
                   ,
            hr(),
#Lisätään "submitButton", jotta kyselyä ei ajeta jokaisen muutoksen jälkeen automaattisesti.
            submitButton("Päivitä",icon("refresh"),width="200px"),
            helpText("Klikkaamalla yllä olevaa painiketta hakusi ajetaan",style ="font-size:15px; color:red")
                                    
                                    
                                    
                                    
                                    
                                    
                        ),
                        
                      
                        mainPanel(
                            h1("Kohderyhmälista"),
                            tableOutput("tableready")
                        )
            )
        ))


Liitetään seuraavaksi Server-puoli mukaan:

shinyServer(function(input,output) {
    
    sqltk = dbConnect(MySQL(), user='root', password='1234', dbname='datamyynnit', host='localhost')
    
    table <- reactive ({
        tol_selected <-  paste0("'",input$tol,"'",collapse=",")
        tuote_selected <- paste0("'",input$tuote,"'",collapse=",")
            #stringr::str_c(stringr::str_c("'",input$tol,"'"), collapse = ',')
        dbGetQuery(sqltk,
                   #paste0("select * from asiakkuudet where Toimiala in (",tol_selected,")")
           
                    gsub(paste0("select asiakkuudet.*, IFNULL(myynnit2.myynniteur,0) as myynniteur from asiakkuudet
                   left join 
                   (
                   SELECT asiakasID as asID, sum(hinta) as myynniteur FROM myynnit where tuote in (",tuote_selected,")
                   and pvm >= '",input$dates[1],"' AND pvm <= '",input$dates[2],"' group by asiakasID 
                   )
                   as myynnit2 on myynnit2.asID=asiakkuudet.asiakasID
                   where asiakkuudet.Toimiala in (", tol_selected,")
                          and  IFNULL(myynnit2.myynniteur,0) >=",input$minmyynti)
                    ,pattern="\n",replacement=" ")
             
                   
        ) })
    
    
    observeEvent(input$printti, {
        showModal(modalDialog(
            title = "Kohderyhmälistasi on nyt valmis!",
            "Muista käyttää minua myös jatkossa :)",
            footer = modalButton("Sulje")
        ))
        #Excelin printtaus
        dttt <- data.frame(table()) 
        dttt$asiakasID <- as.integer(dttt$asiakasID)
       dttt$Puhelinnumero <- as.character(dttt$Puhelinnumero)
#Valitaan sijainti, johon printatut excelit menevät
       write.csv2(dttt,'C:/Users/aleksi/Documents/testila3.csv', row.names = FALSE)
    })
    
    
    output$tableready <- renderTable({
        
        dt <- data.frame(table()) 
        dt$asiakasID <- as.integer(dt$asiakasID)
        dt
        
    })
    

}



)

#Alla oleva koodipätkä katkaisee yhteydet, kun sessio lopetetaan. 
killDbConnections <- function () {
    
    all_cons <- dbListConnections(MySQL())
    
    print(all_cons)
    
    for(con in all_cons)
        +  dbDisconnect(con)
    
    print(paste(length(all_cons), " connections killed."))
    
}
killDbConnections()

Nyt kun työkalu on valmis, katsotaan hieman sen toimintaa. Esim. Valitaan vain ne toimialan "A" yritykset, joilla on ollut viimeisen kuukauden aikana myyntiä tuotteelle 1 vähintään 500€:lla. Alla kuva haun lopputuloksesta. Aikaa filttereiden valitseminen vei vain muutaman sekunnin, joten aikaa säästyi verrattaen, kuin jos oltaisiin pyydetty analyytikkoa hakemaan tämä tieto manuaalisesti tietokannasta. 




maanantai 14. lokakuuta 2019

R-pähkinä


Ajatellaan, että myynti on kiinnostunut erilaisten myyntipiikkien kestosta. Myyntipäällikkö kysyy, minkä pituisia ovat keskimääräiseltä kestoltaan yli 12K €:n päivämyynnit. Hän on siis kiinnostunut siitä, kuinka kauan yli 12K €:n myynti kestää päivinä, kun se raja ylitetään. Esim. Jos viikon aikana tuo raja ylitetään tiistaina ja keskiviikkona, niin rajan ylittävä kesto on 2 päivää. Jos lisäksi raja ylittyy perjantaina, niin rajan ylittävät kestot ovat päivissä laskettuna 2 (ti, ke) ja 1 (pe). Näiden keskiarvo on 1.5 päivää.

Luodaan datasetti, jossa on myynnit sekä päivämäärä:

library(data.table)
library(ggplot2)
pvm <- seq(from=as.Date("2018/09/01"), to=as.Date("2019/09/15"),by=1)
myynti <- rnorm(length(pvm),mean=10000,sd=2000)
dt <- data.table(pvm,myynti)

#Tehdään kuva myynneistä
ggplot(data=dt,aes(x=pvm,y=myynti))+
  geom_line(color="blue")+
  labs(title="Myynnit 10/2018-09/2019",x="Päivämäärä",y="Myynti €")+
  theme(plot.title = element_text(hjust = 0.5),axis.text.x = element_text(angle = 90))+
  geom_hline(yintercept=12000, linetype="dashed", color = "red")
  


Alla myynnit. Myyntiä kiinnostaa yli 12K €:n ylittävät myynnit (kuvassa myyntiraja punaisella).





Tehtävä on hieman pulmallisempi kuin ensin ehkä vaikuttaa. Mistä tiedämme, milloin 12K € ylitetään ja minä päivänä ylitys loppuu? Entä miten osaamme siirtyä seuraavaan myyntipiikkiin, kun aiemman myyntipiikin kesto on laskettu?

Ratkaisu: 

Ongelman saa ratkaistua yllättävän "siististi". Ensin muutan myynnit binäärimuotoon, jotta saamme eristettyä 12K €:n ylittävät myynnit datasta sekä hyödynnettyä myöhemmin cumsum-funktiota.

dt[,myynnitbi := ifelse(myynti >12000,1,0)]

Alla nähdään, kuinka yli 12K € ylittävät myynnit saavat arvon 1.

pvm
myynti
myynnitbi
2018-09-01
10180
0
2018-09-02
12959
1
2018-09-03
9586
0
2018-09-04
11048
0
2018-09-05
9152
0
2018-09-06
9474
0


Seuraavana käytän cumsum-funktiota, koska sillä pystyy saamaan kaikki ne rivit, joissa myyntiraja on ylittynyt. Esim. yllä olevasta cumsum palauttaa kohdan "0 1", eli rivit 1 ja 2. Cumsum käy tämän jälkeen läpi seuraavan rivin ja palauttaa nollan, koska arvo on nolla. Jos cumsum löytää arvon 1, niin se palauttaa kaiken aina nollaan asti. Eli esim. jos meillä on viisi myyntipiikin ylittävää päivää putkeen, niin cumsum palauttaa 011111. Voimme tallentaa jokaisen tällaisen sarjan summan erilliseen vektoriin ja laskea lopuksi keskiarvon. Huom. summan voi ottaa, koska "1" edustaa aina yhtä päivää, eli esim. "011111" tarkoittaa viittä päivää. 


table_ <- head(dt)
for (i in 0:length(table_$myynnitbi)) {
print(table_$myynnitbi[cumsum(table_$myynnitbi==0) ==i])
}
numeric(0)
[1] 0 1
[1] 0
[1] 0
[1] 0
[1] 0
numeric(0)


Ratkaistaan myyntipiikkien keskimääräinen kesto:

 tulosvektori <- NULL #Alustetaan tulosvektori, johon myyntipiikkien kestot tallennetaan
  for (i in 0:length(dt$myynnitbi)) {
    
    tulosvektori[i+1] <- sum(dt$myynnitbi[cumsum(dt$myynnitbi == 0) == i])}
  print(mean(tulosvektori[tulosvektori>0]))
1.156

Vastaus kysymykseen on 1.16 päivää, eli myyntipiikit eivät olleet kestoltaan juuri päivää pidempiä. 



perjantai 16. syyskuuta 2016

Verohallinnon avoimen datan tarkastelua

Tässä tekstissä tutkin Verohallinnon avointa dataa, josta löytyvät tiedot julkisten yhteisöjen tuloverotuksesta vuodelta 2014. Datasta löytyvät mm. y-tunnus, verotuskunta, verotettavat tulot, maksuunpannut verot, ennakkoverot, veronpalautukset sekä jäännösverot. Hyödynnän lisäksi verovelkarekisteriä, josta löytyy tieto, onko yrityksellä verovelkaa yli 10000 euroa. Haluan erityisesti selvittää, onko jäännösverolla ja verovelalla yhteyttä, eli toimiiko jäännösvero indikaattorina tulevaisuuden verovelalle. Suuri jäännösvero voi kertoa yrityksen maksuvaikeuksista tai siitä, että yrityksen tulos on ylittänyt odotukset, jolloin ennakkoveron olisi pitänyt olla suurempi. Toisaalta tällöin yritys olisi voinut maksaa puuttuvat ennakkoveronsa ennakontäydennysmaksuna, jolloin korollista jäännösveroa ei olisi syntynyt. Lisätietoa ennakkoverotuksesta löytyy mm. Verohallinnon sivuilta täältä.

Rajataan tarkastelu aluksi yrityksiin, joilla on verotettavaa tuloa yli 10000 euroa. Näin saadaan aineistoa rajattua suurempiin yrityksiin ja niihin, joilla on jäännösveroa. Lasketaan sitten yhteisö- ja jäännösveroprosentti seuraavasti:

R-koodi:

data <- read.csv(file.choose(), header=F,sep=";",dec=",",skip=1)
colnames(data) <- c("Verovuosi","Ytunnus","Verovelvollisennimi","Verotuskunta","Verotettavatulo","Maksuunpannutverotyhteensa","Ennakotyhteensa", "Veronpalautus","Jaannosvero")
data1 <- data[data$Verotettavatulo>10000&data$Maksuunpannutverotyhteensä>0,]
data1$verotusuhdekaikki <- data1$Maksuunpannutverotyhteensa/data1$Verotettavatulo*100
data1$jaannossuhdekaikki <- data1$Jaannosvero/data1$Maksuunpannutverotyhteensä*100

Katsotaan yhteisöjen tuloveroprosenttien persentiilejä ja nähdään, että mukana on myös yrityksiä, joilla yhteisöveroprosentti on reippaasti yli 20.



Katsotaan vielä histogrammia. Rajataan tarkastelu yrityksiin, joilla tuloveroprosentit ovat välillä 15 ja 30. Yritysten määrä tippuu 57816:sta 57113:een.




Lasketaan seuraavaksi kunnille prosenttiosuudet, joista selviävät, kuinka monta prosenttia kunnan yrityksistä on saanut yli 50%:n verojäämän. Tätä varten tehtään funktio, jota käytetään yhdessä lapply-komennon kanssa. Ennen funktiota muodostetaan uusi dataframe, jossa ovat vain kuntien nimet. Tätä varten pitää Verotuskunta-muuttuja ensin jakaa kahteen osaan, koska se sisältää sekä kuntakoodin että nimen. Tällainen kahtiajako on kätevä tehdä reshape-pakkauksen transform-komennolla.

install.packages("reshape")  # Tehdään muuttujan kahtiajako
library(reshape)
data3 = transform(data3, verotuskunta = colsplit(data3$Verotuskunta, split = " ", names = c('numeric', 'nimi','koodi')))

data4 <- data.frame(unique(data3$verotuskunta.nimi)) # Muodostetaan uusi dataframe, jossa kuntien nimet vain kerran
colnames(data4) <- "Kunta"
data4$Kunta <- as.character(data4$Kunta)

osuus <- function(i) {                                   # Tehdään funktio
a <- length(data3$verotuskunta.nimi[data3$verotuskunta.nimi==i])
b <- length(data3$jaannossuhdekaikki[data3$verotuskunta.nimi==i&data3$jaannossuhdekaikki>=50])
c=b/a*100
return(c)
}
data4$Osuus <- lapply(data4$Kunta,osuus) # Lasketaan prosenttiosuudet joka kunnalle

Tehdään karttakuva, josta lasketut prosenttiosuudet selviävät. Kuvaa varten täytyy ensin hakea koordinaatit, jotka löytyvät esim. täältä. Sitten datat yhdistetään kunnan perusteella. 

R-koodi:

koordinaatit<- read.csv(file.choose(),header=TRUE,dec=".",sep=";")
colnames(koordinaatit)[2] <- "Kunta"
datayhd <- merge(koordinaatit,data4, by="Kunta")
datay <- datayhd[!duplicated(datayhd$Kunta),]
datay$Osuus <- as.numeric(datay$Osuus)
g <- sample(nrow(datay),0.05*nrow(datay))    #Otetaan kuvaa varten 5% havainnoista
datas <- datay[g,]

 map <- get_map(location = 'Finland', zoom = 6)  #Ladataan Suomen kartta 
  mapPoints <- ggmap(map)+
   geom_point(aes(x=Longitude,y=Latitude,size=Osuus),data=datas,alpha=0.5,colour="blue")
  mapPoints
mapPointsLegend <-mapPoints +scale_size(range=c(7,25)) +ggtitle("Jäännösveroprosentti >50: Osuudet Suomen kunnissa")
mapPointsLegend





Katsotaan vielä muutamia suurimpien ja pienimpien osuuksien kuntia. Helsingissä Osuus on ~25%. 

head(datay[order(datay$Osuus),])

          Kunta Postal.Code Latitude Longitude Osuus
123    Hailuoto       90480  65.0000   24.7167     0
624   Kivijärvi       43801  63.0667   25.0500     0
705    Kumlinge       22820  60.2667   20.7833     0
896     Luhanka       19950  61.7833   25.7000     0
899  Lumparland       22630  60.1167   20.2500     0
1298  Savukoski       98800  67.2925   28.1581     0

head(datay[order(-datay$Osuus),])

             Kunta Postal.Code Latitude Longitude     Osuus
909      Merijärvi       86220  64.3000   24.4500 100.00000
118           Geta       22340  60.3833   19.8500  75.00000
1205    Rautavaara       73901  63.4833   28.3000  50.00000
1047 Pelkosenniemi       98500  67.1108   27.5106  41.66667
117          Föglö       22710  60.0167   20.4167  37.50000
1536         Vaala       91700  64.4333   26.8000  36.84211



Seuraavaksi tutkitaan, onko suuren jäännösveroprosentin yrityksillä suurempi todennäköisyys saada verovelkaa. Rajaan tutkittavan joukon verotettavan tulon mukaan välille 50000-80000. Tästä joukosta valitsen ne, joilla on yli 99%:n jäännösvero ja ne, joilla on alle 1%:n jäännösvero. Näistä kummastakin joukosta otan sitten 30 kappaleen satunnaisotannan. 

data33 <- data3[data3$Verotettavatulo>=50000 & data3$Verotettavatulo<=80000,]

datajaannos99 <- data33[data33$jaannossuhdekaikki>99,]
datajaannos1 <- data33[data33$jaannossuhdekaikki <1,]
a <- sample(nrow(datajaannos99),30)
b <- sample(nrow(datajaannos1),30)
datajaannos99t <- datajaannos99[a,]
datajaannos1t <- datajaannos1[b,]

Yli 99%:n jäännösveron otoksessa verovelkaisia oli 7 kappaletta, eli 23.3%:a kaikista. 
Alle 1%:n jäännösveron otoksessa verovelkaisia ei ollut yhtään kappaletta. 

Tehdään vielä Fisherin exact testi. Testillä voidaan testata, onko populaatioiden dikotomisten muuttujien arvojen suhteellisissa osuuksissa eroja.

data = matrix(c(7,23,0,30), ncol=2)
fisher.test(data,alternative = "two.sided")

Fisher's Exact Test for Count Data
data:  data
p-value = 0.01054
alternative hypothesis: true odds ratio is not equal to 1
95 percent confidence interval:
 1.649705      Inf
sample estimates:
odds ratio 
 Inf 

Ratkaistaan myös luottamusväli yli 99%:n ryhmän verovelallisten osuudelle.

prop.test(x=7,n=30,conf.level = 0.95)

1-sample proportions test with continuity correction
data:  7 out of 30, null probability 0.5
X-squared = 7.5, df = 1, p-value = 0.00617
alternative hypothesis: true p is not equal to 0.5
95 percent confidence interval:
 0.1063502 0.4270023
sample estimates:
        p 
0.2333333 

Laskin verovelallisten osuuden lopuksi myös 45-55 %:n jäännösveroryhmälle. 30:n yrityksen otoksessa oli verovelallisia 3 kappaletta, eli 10%:a kaikista.

Verovelkarekisterissä näkyvät tiedot vain viimeiseltä 6 kuukaudelta, joten käytetyn datan (2014) ja verovelkadatan välillä on yli vuoden ero. Tämä antaa viitteitä siitä, että suuri jäännösveroprosentti tänään voi merkitä suurempaa verovelkaisuuden todennäköisyyttä tulevaisuudessa. Ei ole tietenkään suuri yllätys, että alle 1%:n jäännösverolaisilla ei ollut verovelkaa, koska hehän ovat jo maksaneet veronsa ennakkoveroina, jolloin verovelkaa ei voi sillä hetkellä syntyä. Tämä tulos on kuitenkin hyödyllinen, koska se ennakoi verovelvollisen käytöstä tulevaisuudessa. 





torstai 8. syyskuuta 2016

R ja suuret aineistot

Esittelen nyt suurten aineistojen käsittelyä R:llä. Käytän harjoitusaineistona Englannin ja Walesin asuntojen hintadataa, joka löytyy täältä. Datassa on tietoja asuntojen hinnoista Englannissa ja Walesissa vuodesta 1995 lähtien. Koko data sisältää yli 22 miljoonaa riviä ja 16 muuttujaa. Muuttujina löytyvät mm. asunnon hinta, uusi/vanha asunto, tontti oma/vuokralla, myyntipäivämäärä ja asuntotyyppi. Asuntojen koosta ei ole kuitenkaan dataa. Olen kiinnostunut tutkimaan, onko kalleimpien asuinpaikkojen keskihinnoissa ollut eroja vuosina 2014 ja 2015. Rajaan tarkastelun omakotitaloihin, joilla on oma tontti.

Aloitetaan lataamalla data R:ään. Tähän kannattaa käyttää data.table-pakettia, josta löytyy fread-komento. Tämä on huomattavasti nopeampi tapa ladata suuri määrä dataa kuin esim. read.csv. Tarvitsen suuresta datamäärästä dataa vuosille 2014 ja 2015, joten jätän osan datasta lataamatta käyttämällä skip-komentoa. Dataa latautuu 2.5 miljoonaa riviä. Tällaisen määrän dataa R pyörittää vielä hyvin 6GB:n keskusmuistilla.

library(data.table)
data <- fread(file.choose(),header=F,skip=19000000)
colnames(data) <- c("ID","Price","Date","Postcode","Propertytype","Oldnew","Duration","PAON","SAON","Street","Locality","Towncity","District","County","PPD","RS")
data$Price <- as.numeric(data$Price)
data$Date <- as.Date(data$Date, format="%Y-%m-%d")
data$Year <- as.numeric(format(data$Date,"%Y")) # Teen vuosi-muuttujan, jotta vuodet on helppo valita jatkossa.

Nyt kun dataan on tehty sopivat muunnokset, lasketaan ensin uusien omakotitalojen keskihinnat vuosille 2014 ja 2015.

mean(data$Price[data$Year=="2014"  & data$Oldnew=="Y"&data$Duration=="F"&data$Propertytype=="D"]) #Keskihinnat 2014 ja 2015
mean(data$Price[data$Year=="2014"  & data$Oldnew=="Y"&data$Duration=="F"&data$Propertytype=="D"])

Keskihinnat ovat 2014: ~358000 ja 2015: ~373000

Tarkastellaan seuraavaksi keskihintoja maakuntatasolla.

data2015<- do.call(data.frame,aggregate(data$Price[data$Year==2015&data$Propertytype=="D"&data$Duration=="F"],list(data$County[data$Year==2015&data$Propertytype=="D"&data$Duration=="F"],data$Oldnew[data$Year==2015&data$Propertytype=="D"&data$Duration=="F"]),function(x) c(mean=mean(x),length=length(x))))

data2014<- do.call(data.frame,aggregate(data$Price[data$Year==2014&data$Propertytype=="D"&data$Duration=="F"],list(data$County[data$Year==2014&data$Propertytype=="D"&data$Duration=="F"],data$Oldnew[data$Year==2014&data$Propertytype=="D"&data$Duration=="F"]),function(x) c(mean=mean(x),length=length(x))))

Otetaan tarkastelusta pois alueet, joista on alle 30 havaintoa.

data2015l <- subset(data2015, data2015$Length>=30,)
data2014l <- subset(data2014, data2014$Length>=30,)

Järjestetään data keskiarvon mukaan suurimmasta pienimpään.

data2015l <- data2015l[order(-data2015l$Mean),]
data2014l <- data2014l[order(-data2014l$Mean),]

Katsotaan heatmappia, jossa on alhaisen ja korkean keskihinnan maakuntia vuodelta 2015.

datax <- data2015l[c(1:6),]
datay <- data2015l[c(184,189,185,200,191,195.192),]
data5 <- rbind(datax,datay)

library(ggplot2)
ggplot(data = data5, aes(x = Oldnew, y=County, fill=mean)) + 
geom_raster() + scale_fill_gradient2(low="red", mid="yellow", high="green",na.value="grey") +
labs(fill='Keskihinta',title="Asuntojen keskihinnat")






Tehdään seuraavaksi  kuva, jossa näkyvät neljä keskihinnaltaan kalleinta uusien omakotitalojen aluetta vuodelta 2015.

data2015 <- subset(data2015l,data2015l$Oldnew=="Y",)
data2015r <- data2015[,"Mean",drop=FALSE]  #drop=FALSE pitää data.frame -muodon,
row.names(data2015r) <- data2015$County  
data2015tm <- t(as.matrix(data2015r[1:4,,drop=FALSE]))


b <- barplot(data2015tm,,col=c("3"),las=1,yaxt="n",ylab="£",cex.names=0.62,,main="Englannin ja Walesin kalleimmat asuinalueet vuonna 2015 \n (Vertailu: Uusien omakotitalojen keskihinnat)")
axis(2,seq(0,1600000,by=200000))
legend("topright",legend=c("Keskihinta","Kaikkien uusien omakotitalojen keskihintataso"),col=c("3","1"),pch=c(19,NA),lty=c(NA,1))
text(b,60000,round(data2015tm,0))
abline(h=372630,lwd=2)



Katsotaan lopuksi, miten näiden alueiden keskihinnat ovat kehittyneet aiemmasta vuodesta.

data20157<- data2015[1:4,c("County","Mean")]
data2014 <- subset(data2014l,data2014l$Oldnew=="Y",)
yhd <- merge(data20157,data2014,by="County",all.y=F)
yhd$erotus20152014 <- yhd$Mean.x -yhd$Mean.y
yhd$muutosprosentti <- (yhd$erotus20152014/yhd$Mean.y)*100
colnames(yhd) <-c("County","mean2015","Oldnew","mean2014","length","erotus20142014","muutosprosentti")
yy <- yhd[,"muutosprosentti",drop=FALSE]
row.names(yy) <- yhd$County

muutosprosentti <- t(as.matrix(yy))
b <- barplot(muutosprosentti,col=c("5"),las=1,yaxt="n",ylab="%",cex.names=0.62,main="Keskihintojen muutos vuosina 2014-2015")
axis(2,seq(-20,25,by=1))
legend("topright",legend=c("Muutosprosentti"),col=5,pch=19)

text(b,1,round(muutosprosentti,2))



Vaihtelua vuosien välillä on siis melkoisen paljon. Ongelmana tässä datassa on, ettemme tiedä asuntojen kokoa. Asuntojen hintojen kehitystä olisi luontevinta tutkia vertailemalla neliöhintoja alueittain. Suuret hintavaihtelut selittyvät luultavasti osittain sillä, että tutkimme uusia omakotitaloja. Tällaisia taloja myytiin esim. Suur-Lontoossa 223 kappaletta vuonna 2015. Windsorissa vastaava luku oli vain 35. Tällaisilla kalliilla ja harvinaisilla taloilla hintavaihtelut ovat luultavasti suurempia kuin edullisemmilla taloilla, joita on markkinoilla tarjolla koko ajan. Alla on taulukko, jossa on muutosprosentteja vanhoille omakotitaloille kalleimmilla alueilla. Näemme, että luvut ovat Suur-Lontoolle, Poolelle, Surreylle ja Windsorille huomattavasti maltillisemmat. Lisäksi Count-sarakkeiden myydyistä kappalemääristä näemme, että Suur-Lontoossa ja Windsorissa myydään runsaasti enemmän vanhoja kuin uusia omakotitaloja, mikä ei tietenkään ole yllätys. Windsorista käytetyn omakotitalon sai vuonna 2015 halvimmillaan 94000 punnan hintaan, kun taas uudesta joutui maksamaan 325000 puntaa. Kallein myyty omakotitalo maksoi siellä 6.4 miljoonaa puntaa.



2015-2014 muutosprosentit vanhoille omakotitaloille
County Mean2015 Count2015 Mean2014 Count2014Muutosprosentti
GREATER LONDON 947019 5418 917501 5651 3.22
WINDSOR AND MAIDENHEAD 851027 607 812147 640 4.79
SURREY 793702 5742 784542 6032 1.17
HERTFORDSHIRE 723683 3622 677935 3670 6.75
BUCKINGHAMSHIRE 700343 2778 648867 2882 7.93
BRIGHTON AND HOVE 588432 516 588451 544 -0.00
OXFORDSHIRE 562062 2584 532447 2760 5.56
WOKINGHAM 554932 1009 521463 1016 6.42
READING 539202 246 472896 280 14.02
BRACKNELL FOREST 531927 482 466597 495 14.00
WEST BERKSHIRE 527765 782 503797 831 4.76
WEST SUSSEX 491486 4233 468644 4466 4.87
BATH AND NORTH EAST SOMERSET 465811 614 490501 625 -5.03
HAMPSHIRE 463830 7483 440626 7603 5.27
POOLE 459666 1213 426080 1172 7.88 



Alla on kuva Windsorin ja kaikkien alueiden hintajakaumista. Tarkastelu on tässä rajattu alle kahden miljoonan punnan vanhoihin omakotitaloihin. Jakaumat ovat positiivisesti vinoja, joten ei ole yllätys, että kummankin jakauman keskiarvo ylittää niiden mediaaniarvon.



Alla on vielä karttakuva, josta selviävät asuntojen keskihinnat alueittain. Tätä kuvaa varten on ladattava Englannin koordinaatit ja yhdistettävä ne alkuperäiseen dataan. Tämän jälkeen kuva voidaan muodostaa esim. qplotilla tai ggplotilla. Alla olevan kuvan tein qplotilla.








R toimii suurilla aineistolla suhteellisen hyvin, mutta selkeä yläraja alkaa tulla vastaan, kun datan koko ylittää 3 gigaa. Ongelmia suurten aineistojen kanssa tulee varsinkin, jos tavoitteena on mallintaa dataa esim. regressiomalleilla. Tällöin laskemisajat voivat olla jopa tunteja. R:lle on kehitetty pakkaus nimeltä SparkR, joka pyrkii parantamaan R:n toimintaa Big datan kanssa. Pakkaus yhdistää Sparkin ja R:n, eli Sparkia käytetään R:n kautta. Kirjoittelen SparkR:sta jatkossa lisää.