A biblioteca Morpheus foi projetada para facilitar o desenvolvimento de software analítico de alto desempenho envolvendo grandes conjuntos de dados para análise offline e em tempo real na Java Virtual Machine (JVM). A biblioteca é escrita em Java 8 com uso extensivo de lambdas, mas é acessível a todas as linguagens JVM.
Para documentação detalhada com exemplos, veja aqui
Em sua essência, o Morpheus fornece uma estrutura de dados tabulares versátil e eficiente em memória bidimensional chamada DataFrame
, semelhante àquela popularizada pela primeira vez em R. Embora linguagens de computação científica digitadas dinamicamente como R, Python e Matlab sejam ótimas para fazer pesquisas, elas não são bem adequados para sistemas de produção em larga escala, pois se tornam extremamente difíceis de manter e perigosos para refatorar. A biblioteca Morpheus tenta manter o poder e a versatilidade do conceito DataFrame
, ao mesmo tempo em que fornece um conjunto de interfaces muito mais seguro e autodescritivo , o que deve tornar o desenvolvimento, a manutenção e o dimensionamento da complexidade do código muito mais fáceis.
Outra vantagem da biblioteca Morpheus é que ela é extremamente boa no dimensionamento em arquiteturas de processadores multi-core, dados os poderosos recursos de threading da Java Virtual Machine. Muitas operações em um Morpheus DataFrame
podem ser executadas em paralelo perfeitamente, bastando chamar parallel()
na entidade na qual você deseja operar, assim como acontece com Java 8 Streams. Internamente, essas implementações paralelas são baseadas na estrutura Fork & Join, e melhorias quase lineares no desempenho são observadas para certos tipos de operações à medida que núcleos de CPU são adicionados.
Um Morpheus DataFrame
é uma estrutura de armazenamento de colunas onde cada coluna é representada por um Morpheus Array
do qual existem muitas implementações, incluindo versões densas, esparsas e mapeadas em memória. Os arrays Morpheus são otimizados e sempre que possível são apoiados por arrays Java nativos primitivos (mesmo para tipos como LocalDate
, LocalDateTime
etc...), pois são muito mais eficientes do ponto de vista de armazenamento, acesso e coleta de lixo. Morpheus Arrays
mapeados em memória, embora ainda experimentais, permitem que DataFrames
muito grandes sejam criados usando armazenamento fora do heap que é apoiado por arquivos.
Embora o conjunto completo de recursos do Morpheus DataFrame
ainda esteja evoluindo, já existem muitas APIs poderosas para afetar transformações complexas e operações analíticas com facilidade. Existem funções padrão para calcular estatísticas resumidas, realizar vários tipos de regressões lineares, aplicar análise de componentes principais (PCA), para mencionar apenas algumas. O DataFrame
é indexado na dimensão de linha e coluna, permitindo que os dados sejam classificados , fatiados , agrupados e agregados de forma eficiente ao longo de qualquer eixo.
Morpheus também pretende fornecer um mecanismo padrão para carregar conjuntos de dados de vários provedores de dados. A esperança é que esta API seja adotada pela comunidade para aumentar o catálogo de fontes de dados suportadas. Atualmente, os provedores são implementados para permitir o carregamento de dados de Quandl, Federal Reserve, Banco Mundial, Yahoo Finance e Google Finance.
Considere um conjunto de dados de características de veículos motorizados acessível aqui. O código abaixo carrega esses dados CSV em um Morpheus DataFrame
, filtra as linhas para incluir apenas os veículos que têm uma relação potência-peso > 0,1 (onde o peso é convertido em quilogramas) e, em seguida, adiciona uma coluna para registrar a eficiência relativa entre a rodovia e city mileage (MPG), classifica as linhas por esta coluna recém-adicionada em ordem decrescente e, finalmente, registra esse resultado transformado em um arquivo CSV.
DataFrame . read (). csv ( options -> {
options . setResource ( "http://zavtech.com/data/samples/cars93.csv" );
options . setExcludeColumnIndexes ( 0 );
}). rows (). select ( row -> {
double weightKG = row . getDouble ( "Weight" ) * 0.453592d ;
double horsepower = row . getDouble ( "Horsepower" );
return horsepower / weightKG > 0.1d ;
}). cols (). add ( "MPG(Highway/City)" , Double . class , v -> {
double cityMpg = v . row (). getDouble ( "MPG.city" );
double highwayMpg = v . row (). getDouble ( "MPG.highway" );
return highwayMpg / cityMpg ;
}). rows (). sort ( false , "MPG(Highway/City)" ). write (). csv ( options -> {
options . setFile ( "/Users/witdxav/cars93m.csv" );
options . setTitle ( "DataFrame" );
});
Este exemplo demonstra a natureza funcional da API Morpheus, onde muitos tipos de retorno de método são na verdade um DataFrame
e, portanto, permitem esta forma de encadeamento de métodos. Neste exemplo, os métodos csv()
, select()
, add()
e sort()
retornam um quadro. Em alguns casos, o mesmo quadro em que o método opera ou, em outros casos, um filtro ou cópia superficial do quadro em que está sendo operado. As primeiras 10 linhas do conjunto de dados transformado neste exemplo têm a seguinte aparência, com a coluna recém-adicionada aparecendo na extremidade direita do quadro.
Índice | Fabricante | Modelo | Tipo | Preço mínimo | Preço | Preço Máx. | MPG.cidade | MPG.estrada | Airbags | DriveTrain | Cilindros | Tamanho do motor | Potência | RPM | Rev.por.milha | Man.trans.avail | Capacidade.do.tanque.combustível | Passageiros | Comprimento | Distância entre eixos | Largura | Turn.círculo | Espaço.banco.traseiro | Sala.bagagem | Peso | Origem | Faça | MPG(Rodovia/Cidade) | -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- 9 | Cadilac | DeVille | Grande | 33,0000 | 34,7000 | 36,3000 | 16 | 25 | Somente motorista | Frente | 8 | 4,9000 | 200 | 4100 | 1510 | Não | 18.0000 | 6 | 206 | 114 | 73 | 43 | 35 | 18 | 3620 | EUA | Cadillac DeVille | 1,5625 | 10 | Cadilac | Sevilha | Médio porte | 37,5000 | 40.1000 | 42,7000 | 16 | 25 | Motorista e Passageiro | Frente | 8 | 4,6000 | 295 | 6000 | 1985 | Não | 20.0000 | 5 | 204 | 111 | 74 | 44 | 31 | 14 | 3935 | EUA | Cadillac Sevilha | 1,5625 | 70 | Oldsmóvel | Oitenta e oito | Grande | 19,5000 | 20,7000 | 21.9000 | 19 | 28 | Somente motorista | Frente | 6 | 3,8000 | 170 | 4800 | 1570 | Não | 18.0000 | 6 | 201 | 111 | 74 | 42 | 31,5 | 17 | 3470 | EUA | Oldsmobile oitenta e oito | 1.47368421 | 74 | Pontiac | Pássaro de Fogo | Desportivo | 14.0000 | 17.7000 | 21.4000 | 19 | 28 | Motorista e Passageiro | Traseira | 6 | 3,4000 | 160 | 4600 | 1805 | Sim | 15,5000 | 4 | 196 | 101 | 75 | 43 | 25 | 13 | 3240 | EUA | Pontiac Firebird | 1.47368421 | 6 | Buick | LeSabre | Grande | 19.9000 | 20.8000 | 21.7000 | 19 | 28 | Somente motorista | Frente | 6 | 3,8000 | 170 | 4800 | 1570 | Não | 18.0000 | 6 | 200 | 111 | 74 | 42 | 30,5 | 17 | 3470 | EUA | Buick LeSabre | 1.47368421 | 13 | Chevrolet | Camarão | Desportivo | 13,4000 | 15.1000 | 16.8000 | 19 | 28 | Motorista e Passageiro | Traseira | 6 | 3,4000 | 160 | 4600 | 1805 | Sim | 15,5000 | 4 | 193 | 101 | 74 | 43 | 25 | 13 | 3240 | EUA | Chevrolet Camaro | 1.47368421 | 76 | Pontiac | Bonneville | Grande | 19,4000 | 24,4000 | 29,4000 | 19 | 28 | Motorista e Passageiro | Frente | 6 | 3,8000 | 170 | 4800 | 1565 | Não | 18.0000 | 6 | 177 | 111 | 74 | 43 | 30,5 | 18 | 3495 | EUA | Pontiac Bonneville | 1.47368421 | 56 | Mazda | RX-7 | Desportivo | 32,5000 | 32,5000 | 32,5000 | 17 | 25 | Somente motorista | Traseira | rotativo | 1,3000 | 255 | 6500 | 2325 | Sim | 20.0000 | 2 | 169 | 96 | 69 | 37 | NA | NA | 2895 | fora dos EUA | Mazda RX-7 | 1.47058824 | 18 | Chevrolet | Corveta | Desportivo | 34,6000 | 38,0000 | 41,5000 | 17 | 25 | Somente motorista | Traseira | 8 | 5,7000 | 300 | 5000 | 1450 | Sim | 20.0000 | 2 | 179 | 96 | 74 | 43 | NA | NA | 3380 | EUA | Chevrolet Corveta | 1.47058824 | 51 | Lincoln | Cidade_Car | Grande | 34,4000 | 36.1000 | 37,8000 | 18 | 26 | Motorista e Passageiro | Traseira | 8 | 4,6000 | 210 | 4600 | 1840 | Não | 20.0000 | 6 | 219 | 117 | 77 | 45 | 31,5 | 22 | 4055 | EUA | Lincoln Town_Car | 1.44444444 |
A API Morpheus inclui uma interface de regressão para ajustar os dados a um modelo linear usando OLS, WLS ou GLS. O código abaixo usa o mesmo conjunto de dados de carro apresentado no exemplo anterior e regride Horsepower em EngineSize . O exemplo de código imprime os resultados do modelo como padrão, mostrado abaixo, e então cria um gráfico de dispersão com a linha de regressão exibida claramente.
//Load the data
DataFrame < Integer , String > data = DataFrame . read (). csv ( options -> {
options . setResource ( "http://zavtech.com/data/samples/cars93.csv" );
options . setExcludeColumnIndexes ( 0 );
});
//Run OLS regression and plot
String regressand = "Horsepower" ;
String regressor = "EngineSize" ;
data . regress (). ols ( regressand , regressor , true , model -> {
System . out . println ( model );
DataFrame < Integer , String > xy = data . cols (). select ( regressand , regressor );
Chart . create (). withScatterPlot ( xy , false , regressor , chart -> {
chart . title (). withText ( regressand + " regressed on " + regressor );
chart . subtitle (). withText ( "Single Variable Linear Regression" );
chart . plot (). style ( regressand ). withColor ( Color . RED ). withPointsVisible ( true );
chart . plot (). trend ( regressand ). withColor ( Color . BLACK );
chart . plot (). axes (). domain (). label (). withText ( regressor );
chart . plot (). axes (). domain (). format (). withPattern ( "0.00;-0.00" );
chart . plot (). axes (). range ( 0 ). label (). withText ( regressand );
chart . plot (). axes (). range ( 0 ). format (). withPattern ( "0;-0" );
chart . show ();
});
return Optional . empty ();
});
================================================= ============================================ Resultados de regressão linear ================================================= ============================================ Modelo: OLS R-quadrado: 0,5360 Observações: 93 R-Quadrado (ajustado): 0,5309 Modelo DF: 1 Estatística F: 105.1204 Resíduos DF: 91 Estatística F (Prob): 1.11E-16 Erro padrão: 35.8717 Tempo de execução (milis) 52 Durbin-Watson: 1.9591 ================================================= ============================================ Índice | PARÂMETRO | STD_ERROR | T_STAT | P_VALOR | CI_LOWER | CI_UPPER | -------------------------------------------------- -------------------------------------------- Interceptar | 45.2195 | 10.3119 | 4.3852 | 3.107E-5 | 24.736 | 65.7029 | Tamanho do motor | 36.9633 | 3.6052 | 10.2528 | 7.573E-17 | 29.802 | 44.1245 | ================================================= ============================================
É possível acessar todos os registros de transações imobiliárias residenciais do Reino Unido desde 1995 até os dias atuais por meio da iniciativa de Dados Abertos do Governo do Reino Unido. Os dados são apresentados em formato CSV e contêm inúmeras colunas, incluindo informações como data da transação, preço pago, endereço totalmente qualificado (incluindo código postal), tipo de propriedade, tipo de arrendamento e assim por diante.
Vamos começar escrevendo uma função para carregar esses arquivos CSV de buckets do Amazon S3 e, como eles são armazenados um arquivo por ano, fornecemos uma função parametrizada de acordo. Atendendo aos requisitos da nossa análise, não há necessidade de carregar todas as colunas do arquivo, portanto a seguir optamos apenas por ler as colunas nos índices 1, 2, 4 e 11. Além disso, como os arquivos não incluem um cabeçalho , renomeamos as colunas com algo mais significativo para tornar o acesso subsequente um pouco mais claro.
/**
* Loads UK house price from the Land Registry stored in an Amazon S3 bucket
* Note the data does not have a header, so columns will be named Column-0, Column-1 etc...
* @param year the year for which to load prices
* @return the resulting DataFrame, with some columns renamed
*/
private DataFrame < Integer , String > loadHousePrices ( Year year ) {
String resource = "http://prod.publicdata.landregistry.gov.uk.s3-website-eu-west-1.amazonaws.com/pp-%s.csv" ;
return DataFrame . read (). csv ( options -> {
options . setResource ( String . format ( resource , year . getValue ()));
options . setHeader ( false );
options . setCharset ( StandardCharsets . UTF_8 );
options . setIncludeColumnIndexes ( 1 , 2 , 4 , 11 );
options . getFormats (). setParser ( "TransactDate" , Parser . ofLocalDate ( "yyyy-MM-dd HH:mm" ));
options . setColumnNameMapping (( colName , colOrdinal ) -> {
switch ( colOrdinal ) {
case 0 : return "PricePaid" ;
case 1 : return "TransactDate" ;
case 2 : return "PropertyType" ;
case 3 : return "City" ;
default : return colName ;
}
});
});
}
Abaixo utilizamos estes dados para calcular o preço nominal médio (não ajustado pela inflação) de um apartamento para cada ano entre 1995 e 2014 para um subconjunto das maiores cidades do Reino Unido. Existem cerca de 20 milhões de registros no conjunto de dados não filtrados entre 1993 e 2014 e, embora demore bastante para carregar e analisar (aproximadamente 3,5 GB de dados), o Morpheus executa a parte analítica do código em cerca de 5 segundos (sem incluir tempo de carregamento) em um Apple Macbook Pro padrão adquirido no final de 2013. Observe como usamos o processamento paralelo para carregar e processar os dados chamando results.rows().keys().parallel()
.
//Create a data frame to capture the median prices of Apartments in the UK'a largest cities
DataFrame < Year , String > results = DataFrame . ofDoubles (
Range . of ( 1995 , 2015 ). map ( Year :: of ),
Array . of ( "LONDON" , "BIRMINGHAM" , "SHEFFIELD" , "LEEDS" , "LIVERPOOL" , "MANCHESTER" )
);
//Process yearly data in parallel to leverage all CPU cores
results . rows (). keys (). parallel (). forEach ( year -> {
System . out . printf ( "Loading UK house prices for %s... n " , year );
DataFrame < Integer , String > prices = loadHousePrices ( year );
prices . rows (). select ( row -> {
//Filter rows to include only apartments in the relevant cities
final String propType = row . getValue ( "PropertyType" );
final String city = row . getValue ( "City" );
final String cityUpperCase = city != null ? city . toUpperCase () : null ;
return propType != null && propType . equals ( "F" ) && results . cols (). contains ( cityUpperCase );
}). rows (). groupBy ( "City" ). forEach ( 0 , ( groupKey , group ) -> {
//Group row filtered frame so we can compute median prices in selected cities
final String city = groupKey . item ( 0 );
final double priceStat = group . colAt ( "PricePaid" ). stats (). median ();
results . data (). setDouble ( year , city , priceStat );
});
});
//Map row keys to LocalDates, and map values to be percentage changes from start date
final DataFrame < LocalDate , String > plotFrame = results . mapToDoubles ( v -> {
final double firstValue = v . col (). getDouble ( 0 );
final double currentValue = v . getDouble ();
return ( currentValue / firstValue - 1d ) * 100d ;
}). rows (). mapKeys ( row -> {
final Year year = row . key ();
return LocalDate . of ( year . getValue (), 12 , 31 );
});
//Create a plot, and display it
Chart . create (). withLinePlot ( plotFrame , chart -> {
chart . title (). withText ( "Median Nominal House Price Changes" );
chart . title (). withFont ( new Font ( "Arial" , Font . BOLD , 14 ));
chart . subtitle (). withText ( "Date Range: 1995 - 2014" );
chart . plot (). axes (). domain (). label (). withText ( "Year" );
chart . plot (). axes (). range ( 0 ). label (). withText ( "Percent Change from 1995" );
chart . plot (). axes (). range ( 0 ). format (). withPattern ( "0.##'%';-0.##'%'" );
chart . plot (). style ( "LONDON" ). withColor ( Color . BLACK );
chart . legend (). on (). bottom ();
chart . show ();
});
A variação percentual nos preços nominais medianos dos apartamentos no subconjunto de cidades escolhidas é mostrada no gráfico abaixo. Mostra que Londres não sofreu qualquer queda nominal dos preços da habitação como resultado da Crise Financeira Global (GFC), mas nem todas as cidades do Reino Unido se mostraram tão resilientes. O que é ligeiramente surpreendente é que algumas das cidades menos ricas do norte registaram uma taxa de valorização mais elevada no período de 2003 a 2006, em comparação com Londres. Uma coisa a notar é que, embora Londres não tenha registado qualquer redução no preço nominal, houve certamente uma correcção bastante severa em termos de EUR e USD, uma vez que a libra esterlina desvalorizou fortemente face a estas moedas durante o GFC.
A visualização de dados no Morpheus DataFrames
é facilitada por meio de uma API simples de abstração de gráficos com adaptadores que suportam JFreeChart e Google Charts (com outros a serem seguidos por demanda popular). Este design torna possível gerar gráficos Java Swing interativos, bem como gráficos baseados em navegador HTML5, por meio da mesma interface programática. Para obter mais detalhes sobre como usar esta API, consulte a seção sobre visualização aqui e o código aqui.
Morpheus é publicado no Maven Central para que possa ser facilmente adicionado como uma dependência na ferramenta de construção de sua escolha. A base de código está atualmente dividida em 5 repositórios para permitir que cada módulo possa evoluir de forma independente. O módulo principal, apropriadamente denominado morpheus-core, é a biblioteca fundamental da qual todos os outros módulos dependem. Os vários artefatos do Maven são os seguintes:
Núcleo de Morfeu
A biblioteca básica que contém Morpheus Arrays, DataFrames e outras interfaces e implementações importantes.
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-core</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Visualização de Morfeu
Os componentes de visualização para exibir DataFrames
em gráficos e tabelas.
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-viz</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morfeu Quandl
O adaptador para carregar dados do Quandl
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-quandl</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morfeu Google
O adaptador para carregar dados do Google Finance
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-google</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morfeu Yahoo
O adaptador para carregar dados do Yahoo Finance
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-yahoo</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Um fórum de perguntas e respostas foi configurado usando os Grupos do Google e pode ser acessado aqui
Morpheus Javadocs pode ser acessado online aqui.
Um servidor de construção de Integração Contínua pode ser acessado aqui, que constrói código após cada mesclagem.
Morpheus é lançado sob a licença Apache Software Foundation versão 2.