La biblioteca Morpheus está diseñada para facilitar el desarrollo de software analítico de alto rendimiento que involucra grandes conjuntos de datos para análisis fuera de línea y en tiempo real en la Máquina Virtual Java (JVM). La biblioteca está escrita en Java 8 con un uso extensivo de lambdas, pero es accesible a todos los lenguajes JVM.
Para obtener documentación detallada con ejemplos, consulte aquí
En esencia, Morpheus proporciona una estructura de datos tabular versátil y eficiente en memoria bidimensional llamada DataFrame
, similar a la que se popularizó por primera vez en R. Si bien los lenguajes de computación científica tipados dinámicamente como R, Python y Matlab son excelentes para realizar investigaciones, no lo son. Adecuado para sistemas de producción a gran escala, ya que se vuelven extremadamente difíciles de mantener y peligrosos de refactorizar. La biblioteca Morpheus intenta conservar el poder y la versatilidad del concepto DataFrame
, al tiempo que proporciona un conjunto de interfaces mucho más seguro y autodescriptivo , lo que debería facilitar mucho el desarrollo, mantenimiento y escalado de la complejidad del código.
Otra ventaja de la biblioteca Morpheus es que es extremadamente buena para escalar en arquitecturas de procesadores multinúcleo dadas las potentes capacidades de subprocesos de la máquina virtual Java. Muchas operaciones en un Morpheus DataFrame
se pueden ejecutar en paralelo sin problemas simplemente llamando parallel()
en la entidad en la que desea operar, al igual que con Java 8 Streams. Internamente, estas implementaciones paralelas se basan en el marco Fork & Join y se observan mejoras casi lineales en el rendimiento para ciertos tipos de operaciones a medida que se agregan núcleos de CPU.
Un Morpheus DataFrame
es una estructura de almacenamiento de columnas donde cada columna está representada por un Morpheus Array
del cual existen muchas implementaciones, incluidas versiones densas, dispersas y mapeadas en memoria. Los arreglos de Morpheus están optimizados y, siempre que sea posible, están respaldados por arreglos Java nativos primitivos (incluso para tipos como LocalDate
, LocalDateTime
, etc.), ya que son mucho más eficientes desde una perspectiva de almacenamiento, acceso y recolección de basura. Los Morpheus Arrays
mapeados en memoria, aunque aún son experimentales, permiten la creación DataFrames
muy grandes utilizando almacenamiento fuera del montón respaldado por archivos.
Si bien el conjunto completo de funciones de Morpheus DataFrame
aún está evolucionando, ya existen muchas API potentes para realizar transformaciones complejas y operaciones analíticas con facilidad. Existen funciones estándar para calcular estadísticas resumidas, realizar varios tipos de regresiones lineales y aplicar análisis de componentes principales (PCA), por mencionar solo algunas. El DataFrame
está indexado tanto en la dimensión de fila como en la de columna, lo que permite ordenar , dividir , agrupar y agregar datos de manera eficiente a lo largo de cualquier eje.
Morpheus también pretende proporcionar un mecanismo estándar para cargar conjuntos de datos de varios proveedores de datos. La esperanza es que la comunidad adopte esta API para ampliar el catálogo de fuentes de datos compatibles. Actualmente, se implementan proveedores para permitir la carga de datos desde Quandl, la Reserva Federal, el Banco Mundial, Yahoo Finance y Google Finance.
Considere un conjunto de datos de características de vehículos de motor accesibles aquí. El siguiente código carga estos datos CSV en un Morpheus DataFrame
, filtra las filas para incluir solo aquellos vehículos que tienen una relación potencia-peso > 0,1 (donde el peso se convierte en kilogramos) y luego agrega una columna para registrar la eficiencia relativa entre la carretera y kilometraje en ciudad (MPG), ordena las filas según esta columna recién agregada en orden descendente y finalmente registra este resultado transformado en un archivo 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 ejemplo demuestra la naturaleza funcional de la API de Morpheus, donde muchos tipos de retorno de métodos son de hecho un DataFrame
y, por lo tanto, permiten esta forma de encadenamiento de métodos. En este ejemplo, los métodos csv()
, select()
, add()
y sort()
devuelven un marco. En algunos casos, el mismo marco en el que opera el método, o en otros casos, un filtro o una copia superficial del marco en el que se opera. Las primeras 10 filas del conjunto de datos transformado en este ejemplo tienen el siguiente aspecto, y la columna recién agregada aparece en el extremo derecho del marco.
Índice | Fabricante | Modelo | Tipo | Precio mínimo | Precio | Precio máx. | MPG.ciudad | MPG.carretera | Bolsas de aire | Tren motriz | Cilindros | Tamaño del motor | Caballos de fuerza | RPM | Revoluciones por milla | Hombre.trans.disponible | Capacidad.del.tanque.de.combustible | Pasajeros | Longitud | Distancia entre ejes | Ancho | Girar.círculo | Espacio.para.el.asiento.trasero | Cuarto de equipaje | Peso | Origen | Hacer | MPG (carretera/ciudad) | -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- 9 | Cadillac | DeVille | Grande | 33.0000 | 34.7000 | 36.3000 | 16 | 25 | Sólo conductor | Frente | 8 | 4.9000 | 200 | 4100 | 1510 | No | 18.0000 | 6 | 206 | 114 | 73 | 43 | 35 | 18 | 3620 | Estados Unidos | Cadillac DeVille | 1.5625 | 10 | Cadillac | Sevilla | Tamaño mediano | 37.5000 | 40.1000 | 42.7000 | 16 | 25 | Conductor y Pasajero | Frente | 8 | 4.6000 | 295 | 6000 | 1985 | No | 20.0000 | 5 | 204 | 111 | 74 | 44 | 31 | 14 | 3935 | Estados Unidos | Cadillac Sevilla | 1.5625 | 70 | Oldsmobile | Ochenta y ocho | Grande | 19.5000 | 20.7000 | 21.9000 | 19 | 28 | Sólo conductor | Frente | 6 | 3.8000 | 170 | 4800 | 1570 | No | 18.0000 | 6 | 201 | 111 | 74 | 42 | 31,5 | 17 | 3470 | Estados Unidos | Oldsmobile ochenta y ocho | 1.47368421 | 74 | Pontiac | pájaro de fuego | Deportivo | 14.0000 | 17.7000 | 21.4000 | 19 | 28 | Conductor y Pasajero | Trasero | 6 | 3.4000 | 160 | 4600 | 1805 | Sí | 15.5000 | 4 | 196 | 101 | 75 | 43 | 25 | 13 | 3240 | Estados Unidos | Pontiac Firebird | 1.47368421 | 6 | Buick | LeSabre | Grande | 19.9000 | 20.8000 | 21.7000 | 19 | 28 | Sólo conductor | Frente | 6 | 3.8000 | 170 | 4800 | 1570 | No | 18.0000 | 6 | 200 | 111 | 74 | 42 | 30,5 | 17 | 3470 | Estados Unidos | Buick LeSabre | 1.47368421 | 13 | Chevrolet | Camaro | Deportivo | 13.4000 | 15.1000 | 16.8000 | 19 | 28 | Conductor y Pasajero | Trasero | 6 | 3.4000 | 160 | 4600 | 1805 | Sí | 15.5000 | 4 | 193 | 101 | 74 | 43 | 25 | 13 | 3240 | Estados Unidos | Chevrolet Camaro | 1.47368421 | 76 | Pontiac | Bonneville | Grande | 19.4000 | 24.4000 | 29.4000 | 19 | 28 | Conductor y Pasajero | Frente | 6 | 3.8000 | 170 | 4800 | 1565 | No | 18.0000 | 6 | 177 | 111 | 74 | 43 | 30,5 | 18 | 3495 | Estados Unidos | Pontiac Bonneville | 1.47368421 | 56 | Mazda | RX-7 | Deportivo | 32.5000 | 32.5000 | 32.5000 | 17 | 25 | Sólo conductor | Trasero | rotativo | 1,3000 | 255 | 6500 | 2325 | Sí | 20.0000 | 2 | 169 | 96 | 69 | 37 | NA | NA | 2895 | fuera de EE. UU. | Mazda RX-7 | 1.47058824 | 18 | Chevrolet | Corbeta | Deportivo | 34.6000 | 38.0000 | 41.5000 | 17 | 25 | Sólo conductor | Trasero | 8 | 5.7000 | 300 | 5000 | 1450 | Sí | 20.0000 | 2 | 179 | 96 | 74 | 43 | NA | NA | 3380 | Estados Unidos | Chevrolet Corbeta | 1.47058824 | 51 | Lincoln | Town_Car | Grande | 34.4000 | 36.1000 | 37.8000 | 18 | 26 | Conductor y Pasajero | Trasero | 8 | 4.6000 | 210 | 4600 | 1840 | No | 20.0000 | 6 | 219 | 117 | 77 | 45 | 31,5 | 22 | 4055 | Estados Unidos | Lincoln Town_Car | 1.44444444 |
La API de Morpheus incluye una interfaz de regresión para ajustar los datos a un modelo lineal utilizando OLS, WLS o GLS. El siguiente código utiliza el mismo conjunto de datos de automóviles presentado en el ejemplo anterior y realiza una regresión de Horsepower en EngineSize . El ejemplo de código imprime los resultados del modelo según el estándar, que se muestra a continuación, y luego crea un gráfico de dispersión con la línea de regresión claramente mostrada.
//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 regresión lineal ==================================================== =============================================== Modelo: OLS R cuadrado: 0.5360 Observaciones: 93 R-Cuadrado (ajustado): 0,5309 Modelo DF: 1 Estadística F: 105.1204 Residuos del DF: 91 Estadística F (Prob): 1.11E-16 Error estándar: 35.8717 Tiempo de ejecución (milis) 52 Durbin-Watson: 1,9591 ==================================================== =============================================== Índice | PARÁMETRO | ERROR_ESTD | T_STAT | P_VALOR | CI_BAJO | CI_SUPERIOR | -------------------------------------------------- -------------------------------------- Interceptar | 45.2195 | 10.3119 | 4.3852 | 3.107E-5 | 24.736 | 65.7029 | Tamaño del motor | 36.9633 | 3.6052 | 10.2528 | 7.573E-17 | 29.802 | 44.1245 | ==================================================== ===============================================
Es posible acceder a todos los registros de transacciones de bienes raíces residenciales del Reino Unido desde 1995 hasta la actualidad a través de la iniciativa de Datos Abiertos del Gobierno del Reino Unido. Los datos se presentan en formato CSV y contienen numerosas columnas, incluida información como la fecha de la transacción, el precio pagado, la dirección completa (incluido el código postal), el tipo de propiedad, el tipo de arrendamiento, etc.
Comencemos escribiendo una función para cargar estos archivos CSV desde depósitos de Amazon S3 y, dado que se almacenan un archivo por año, proporcionamos una función parametrizada en consecuencia. Dados los requisitos de nuestro análisis, no es necesario cargar todas las columnas del archivo, por lo que a continuación solo elegimos leer las columnas en el índice 1, 2, 4 y 11. Además, dado que los archivos no incluyen un encabezado , cambiamos el nombre de las columnas por algo más significativo para que el acceso posterior sea un poco más 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 ;
}
});
});
}
A continuación utilizamos estos datos para calcular el precio nominal medio (no ajustado a la inflación) de un apartamento para cada año entre 1995 y 2014 para un subconjunto de las ciudades más grandes del Reino Unido. Hay alrededor de 20 millones de registros en el conjunto de datos sin filtrar entre 1993 y 2014, y aunque lleva bastante tiempo cargar y analizar (aproximadamente 3,5 GB de datos), Morpheus ejecuta la parte analítica del código en aproximadamente 5 segundos (sin incluir tiempo de carga) en un Apple Macbook Pro estándar comprado a finales de 2013. Observe cómo utilizamos el procesamiento paralelo para cargar y procesar los datos llamando 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 ();
});
El cambio porcentual en los precios medios nominales de los apartamentos en el subconjunto de ciudades elegidas se muestra en el siguiente gráfico. Muestra que Londres no sufrió ninguna caída en el precio nominal de la vivienda como resultado de la crisis financiera global (GFC), sin embargo, no todas las ciudades del Reino Unido demostraron ser resilientes. Lo que resulta ligeramente sorprendente es que algunas de las ciudades menos prósperas del norte experimentaron una mayor tasa de apreciación en el período 2003-2006 en comparación con Londres. Una cosa a tener en cuenta es que, si bien Londres no experimentó ninguna reducción de los precios nominales, ciertamente hubo una corrección bastante severa en términos de EUR y USD, ya que la libra esterlina se depreció fuertemente frente a estas monedas durante la GFC.
La visualización de datos en Morpheus DataFrames
se facilita a través de una API de abstracción de gráficos simple con adaptadores que admiten tanto JFreeChart como Google Charts (y otros seguirán según la demanda popular). Este diseño hace posible generar gráficos Java Swing interactivos, así como gráficos basados en navegador HTML5 a través de la misma interfaz programática. Para obtener más detalles sobre cómo utilizar esta API, consulte la sección sobre visualización aquí y el código aquí.
Morpheus se publica en Maven Central, por lo que se puede agregar fácilmente como una dependencia en la herramienta de compilación que elija. Actualmente, el código base está dividido en 5 repositorios para permitir que cada módulo evolucione de forma independiente. El módulo principal, que acertadamente se llama morpheus-core, es la biblioteca fundamental de la que dependen todos los demás módulos. Los diversos artefactos de Maven son los siguientes:
Núcleo de Morfeo
La biblioteca fundamental que contiene Morpheus Arrays, DataFrames y otras interfaces e implementaciones clave.
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-core</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Visualización de Morfeo
Los componentes de visualización para mostrar DataFrames
en gráficos y tablas.
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-viz</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morfeo Quandl
El adaptador para cargar datos de Quandl
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-quandl</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morfeo Google
El adaptador para cargar datos de Google Finance
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-google</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morfeo Yahoo
El adaptador para cargar datos de Yahoo Finanzas
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-yahoo</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Se ha configurado un foro de Preguntas y respuestas mediante Grupos de Google y se puede acceder a él aquí.
Se puede acceder a Morpheus Javadocs en línea aquí.
Aquí se puede acceder a un servidor de compilación de integración continua, que genera código después de cada fusión.
Morpheus se publica bajo la licencia Apache Software Foundation versión 2.