La bibliothèque Morpheus est conçue pour faciliter le développement de logiciels analytiques hautes performances impliquant de grands ensembles de données pour une analyse hors ligne et en temps réel sur la machine virtuelle Java (JVM). La bibliothèque est écrite en Java 8 avec une utilisation intensive de lambdas, mais est accessible à tous les langages JVM.
Pour une documentation détaillée avec des exemples, voir ici
À la base, Morpheus fournit une structure de données tabulaire bidimensionnelle polyvalente et économe en mémoire appelée DataFrame
, similaire à celle popularisée pour la première fois dans R. Bien que les langages de calcul scientifique typés dynamiquement comme R, Python et Matlab soient parfaits pour faire de la recherche, ils ne le sont pas. adaptés aux systèmes de production à grande échelle car ils deviennent extrêmement difficiles à maintenir et dangereux à refactoriser. La bibliothèque Morpheus tente de conserver la puissance et la polyvalence du concept DataFrame
, tout en fournissant un ensemble d'interfaces beaucoup plus sûrs et auto-descriptifs , ce qui devrait faciliter le développement, la maintenance et la mise à l'échelle de la complexité du code.
Un autre avantage de la bibliothèque Morpheus est qu'elle est extrêmement efficace pour évoluer sur des architectures de processeurs multicœurs, compte tenu des puissantes capacités de thread de la machine virtuelle Java. De nombreuses opérations sur un Morpheus DataFrame
peuvent être exécutées en parallèle de manière transparente en appelant simplement parallel()
sur l'entité sur laquelle vous souhaitez opérer, un peu comme avec Java 8 Streams. En interne, ces implémentations parallèles sont basées sur le framework Fork & Join, et des améliorations quasi linéaires des performances sont observées pour certains types d'opérations à mesure que des cœurs de processeur sont ajoutés.
Un Morpheus DataFrame
est une structure de magasin de colonnes où chaque colonne est représentée par un Morpheus Array
dont il existe de nombreuses implémentations, y compris des versions denses, clairsemées et mappées en mémoire. Les tableaux Morpheus sont optimisés et, dans la mesure du possible, sont soutenus par des tableaux Java natifs primitifs (même pour des types tels que LocalDate
, LocalDateTime
etc...) car ils sont beaucoup plus efficaces du point de vue du stockage, de l'accès et du garbage collection. Les Morpheus Arrays
mappés en mémoire, bien qu'encore expérimentaux, permettent de créer de très grands DataFrames
à l'aide d'un stockage hors tas sauvegardé par des fichiers.
Bien que l'ensemble complet des fonctionnalités de Morpheus DataFrame
soit encore en évolution, il existe déjà de nombreuses API puissantes permettant d'effectuer facilement des transformations complexes et des opérations analytiques. Il existe des fonctions standard pour calculer des statistiques récapitulatives, effectuer différents types de régressions linéaires, appliquer l'analyse en composantes principales (ACP), pour n'en citer que quelques-unes. Le DataFrame
est indexé à la fois dans la dimension de ligne et de colonne, permettant aux données d'être triées , découpées , regroupées et agrégées efficacement le long de chaque axe.
Morpheus vise également à fournir un mécanisme standard pour charger des ensembles de données provenant de divers fournisseurs de données. L'espoir est que cette API soit adoptée par la communauté afin d'élargir le catalogue de sources de données prises en charge. Actuellement, des fournisseurs sont mis en œuvre pour permettre le chargement de données depuis Quandl, la Réserve fédérale, la Banque mondiale, Yahoo Finance et Google Finance.
Considérons un ensemble de données sur les caractéristiques des véhicules automobiles accessible ici. Le code ci-dessous charge ces données CSV dans un Morpheus DataFrame
, filtre les lignes pour inclure uniquement les véhicules qui ont un rapport puissance/poids > 0,1 (où le poids est converti en kilogrammes), puis ajoute une colonne pour enregistrer l'efficacité relative entre l'autoroute et kilométrage de la ville (MPG), trie les lignes en fonction de cette colonne nouvellement ajoutée par ordre décroissant et enregistre enfin ce résultat transformé dans un fichier 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" );
});
Cet exemple démontre la nature fonctionnelle de l'API Morpheus, où de nombreux types de retour de méthode sont en fait un DataFrame
et permettent donc cette forme de chaînage de méthodes. Dans cet exemple, les méthodes csv()
, select()
, add()
et sort()
renvoient toutes un cadre. Dans certains cas, il s'agit de la même image sur laquelle la méthode opère, ou dans d'autres cas, d'un filtre ou d'une copie superficielle de l'image sur laquelle opère la méthode. Les 10 premières lignes de l'ensemble de données transformé dans cet exemple se présentent comme suit, la colonne nouvellement ajoutée apparaissant à l'extrême droite du cadre.
Indice | Fabricant | Modèle | Tapez | Prix minimum | Prix | Prix Max. | MPG.ville | MPG.autoroute | Coussins gonflables | Groupe motopropulseur | Cylindres | Taille du moteur | Puissance | RPM | Rev.par.mile | Man.trans.disponibilité | Capacité.du.réservoir.de.carburant | Passagers | Longueur | Empattement | Largeur | Tourner.cercle | Espace.aux.places.arrière | Bagagerie.chambre | Poids | Origine | Faire | MPG (autoroute/ville) | -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- 9 | Cadillac | De Ville | Grand | 33.0000 | 34.7000 | 36.3000 | 16 | 25 | Conducteur uniquement | Avant | 8 | 4.9000 | 200 | 4100 | 1510 | Non | 18.0000 | 6 | 206 | 114 | 73 | 43 | 35 | 18 | 3620 | États-Unis | Cadillac DeVille | 1,5625 | 10 | Cadillac | Séville | Taille moyenne | 37.5000 | 40.1000 | 42.7000 | 16 | 25 | Conducteur et passager | Avant | 8 | 4,6000 | 295 | 6000 | 1985 | Non | 20.0000 | 5 | 204 | 111 | 74 | 44 | 31 | 14 | 3935 | États-Unis | Cadillac Séville | 1,5625 | 70 | Oldsmobile | Quatre-vingt-huit | Grand | 19.5000 | 20.7000 | 21.9000 | 19 | 28 | Conducteur uniquement | Avant | 6 | 3.8000 | 170 | 4800 | 1570 | Non | 18.0000 | 6 | 201 | 111 | 74 | 42 | 31,5 | 17 | 3470 | États-Unis | Oldsmobile quatre-vingt-huit | 1.47368421 | 74 | Pontiac | Oiseau de feu | Sportif | 14.0000 | 17.7000 | 21.4000 | 19 | 28 | Conducteur et passager | Arrière | 6 | 3.4000 | 160 | 4600 | 1805 | Oui | 15.5000 | 4 | 196 | 101 | 75 | 43 | 25 | 13 | 3240 | États-Unis | Pontiac Firebird | 1.47368421 | 6 | Chevrolet | LeSabre | Grand | 19.9000 | 20.8000 | 21.7000 | 19 | 28 | Conducteur uniquement | Avant | 6 | 3.8000 | 170 | 4800 | 1570 | Non | 18.0000 | 6 | 200 | 111 | 74 | 42 | 30,5 | 17 | 3470 | États-Unis | Buick LeSabre | 1.47368421 | 13 | Chevrolet | Camaro | Sportif | 13.4000 | 15.1000 | 16.8000 | 19 | 28 | Conducteur et passager | Arrière | 6 | 3.4000 | 160 | 4600 | 1805 | Oui | 15.5000 | 4 | 193 | 101 | 74 | 43 | 25 | 13 | 3240 | États-Unis | Chevrolet Camaro | 1.47368421 | 76 | Pontiac | Bonneville | Grand | 19.4000 | 24.4000 | 29.4000 | 19 | 28 | Conducteur et passager | Avant | 6 | 3.8000 | 170 | 4800 | 1565 | Non | 18.0000 | 6 | 177 | 111 | 74 | 43 | 30,5 | 18 | 3495 | États-Unis | Pontiac Bonneville | 1.47368421 | 56 | Mazda | RX-7 | Sportif | 32.5000 | 32.5000 | 32.5000 | 17 | 25 | Conducteur uniquement | Arrière | rotatif | 1,3000 | 255 | 6500 | 2325 | Oui | 20.0000 | 2 | 169 | 96 | 69 | 37 | NA | NA | 2895 | non-USA | Mazda RX-7 | 1.47058824 | 18 | Chevrolet | Corvette | Sportif | 34.6000 | 38.0000 | 41.5000 | 17 | 25 | Conducteur uniquement | Arrière | 8 | 5,7000 | 300 | 5000 | 1450 | Oui | 20.0000 | 2 | 179 | 96 | 74 | 43 | NA | NA | 3380 | États-Unis | Chevrolet Corvette | 1.47058824 | 51 | Lincoln | Ville_Car | Grand | 34.4000 | 36.1000 | 37.8000 | 18 | 26 | Conducteur et passager | Arrière | 8 | 4,6000 | 210 | 4600 | 1840 | Non | 20.0000 | 6 | 219 | 117 | 77 | 45 | 31,5 | 22 | 4055 | États-Unis | Lincoln Town_Car | 1.44444444 |
L'API Morpheus comprend une interface de régression afin d'ajuster les données à un modèle linéaire en utilisant soit OLS, WLS ou GLS. Le code ci-dessous utilise le même ensemble de données de voiture introduit dans l'exemple précédent et régresse Horsepower sur EngineSize . L'exemple de code imprime les résultats du modèle sur la sortie standard, illustrée ci-dessous, puis crée un nuage de points avec la ligne de régression clairement affichée.
//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 ();
});
=================================================== ============================================= Résultats de la régression linéaire =================================================== ============================================= Modèle : MCO R-carré : 0,5360 Observations : 93 R-carré (ajusté) : 0,5309 Modèle DF : 1 Statistique F : 105.1204 Résidus DF : 91 Statistique F (Prob) : 1,11E-16 Erreur standard : 35,8717 Durée d'exécution (millis) 52 Durbin-Watson : 1,9591 =================================================== ============================================= Indice | PARAMÈTRE | STD_ERREUR | T_STAT | P_VALEUR | CI_INFÉRIEUR | CI_UPPER | -------------------------------------------------- -------------------------------------------- Intercepter | 45.2195 | 10.3119 | 4.3852 | 3.107E-5 | 24.736 | 65.7029 | Taille du moteur | 36.9633 | 3.6052 | 10.2528 | 7.573E-17 | 29.802 | 44.1245 | =================================================== =============================================
Il est possible d'accéder à tous les enregistrements de transactions immobilières résidentielles au Royaume-Uni de 1995 à aujourd'hui via l'initiative Open Data du gouvernement britannique. Les données sont présentées au format CSV et contiennent de nombreuses colonnes, comprenant des informations telles que la date de la transaction, le prix payé, l'adresse complète (y compris le code postal), le type de propriété, le type de bail, etc.
Commençons par écrire une fonction pour charger ces fichiers CSV à partir des compartiments Amazon S3, et comme ils sont stockés un fichier par an, nous fournissons une fonction paramétrée en conséquence. Compte tenu des exigences de notre analyse, il n'est pas nécessaire de charger toutes les colonnes du fichier, nous choisissons donc ci-dessous de lire uniquement les colonnes aux index 1, 2, 4 et 11. De plus, comme les fichiers n'incluent pas d'en-tête , nous renommeons les colonnes en quelque chose de plus significatif pour rendre les accès ultérieurs un peu plus clairs.
/**
* 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 ;
}
});
});
}
Ci-dessous, nous utilisons ces données afin de calculer le prix nominal médian (non corrigé de l'inflation) d'un appartement pour chaque année entre 1995 et 2014 pour un sous-ensemble des plus grandes villes du Royaume-Uni. Il y a environ 20 millions d'enregistrements dans l'ensemble de données non filtrées entre 1993 et 2014, et bien que le chargement et l'analyse prennent un temps assez long (environ 3,5 Go de données), Morpheus exécute la partie analytique du code en 5 secondes environ (sans compter temps de chargement) sur un Apple Macbook Pro standard acheté fin 2013. Notez comment nous utilisons le traitement parallèle pour charger et traiter les données en appelant 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 ();
});
La variation en pourcentage des prix nominaux médians des appartements dans le sous-ensemble des villes choisies est présentée dans le graphique ci-dessous. Il montre que Londres n’a subi aucune baisse du prix nominal de l’immobilier à la suite de la crise financière mondiale (GFC), mais que toutes les villes du Royaume-Uni n’ont pas fait preuve d’une telle résilience. Ce qui est légèrement surprenant, c'est que certaines des villes du Nord les moins riches ont connu un taux d'appréciation plus élevé entre 2003 et 2006 que Londres. Une chose à noter est que même si Londres n’a constaté aucune réduction des prix nominaux, il y a certainement eu une correction assez sévère en termes d’EUR et d’USD puisque la livre sterling s’est fortement dépréciée par rapport à ces devises pendant la GFC.
La visualisation des données dans Morpheus DataFrames
est facilitée via une simple API d'abstraction de graphiques avec des adaptateurs prenant en charge à la fois JFreeChart et Google Charts (avec d'autres à suivre à la demande générale). Cette conception permet de générer des graphiques Java Swing interactifs ainsi que des graphiques basés sur un navigateur HTML5 via la même interface de programmation. Pour plus de détails sur l'utilisation de cette API, consultez la section sur la visualisation ici et le code ici.
Morpheus est publié sur Maven Central afin qu'il puisse être facilement ajouté en tant que dépendance dans l'outil de build de votre choix. La base de code est actuellement divisée en 5 référentiels pour permettre à chaque module d'évoluer indépendamment. Le module principal, qui porte bien son nom morpheus-core, est la bibliothèque fondamentale dont dépendent tous les autres modules. Les différents artefacts Maven sont les suivants :
Noyau de Morphée
La bibliothèque fondamentale qui contient des tableaux Morpheus, des DataFrames et d'autres interfaces et implémentations clés.
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-core</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Visualisation Morphée
Les composants de visualisation pour afficher DataFrames
dans des graphiques et des tableaux.
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-viz</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morphée Quandl
L'adaptateur pour charger les données de Quandl
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-quandl</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morphée Google
L'adaptateur pour charger les données de Google Finance
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-google</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Morphée Yahoo
L'adaptateur pour charger les données de Yahoo Finance
< dependency >
< groupId >com.zavtech</ groupId >
< artifactId >morpheus-yahoo</ artifactId >
< version >${VERSION}</ version >
</ dependency >
Un forum Questions & Réponses a été mis en place à l'aide de Google Groupes et est accessible ici
Les Javadocs Morpheus sont accessibles en ligne ici.
Un serveur de build d'intégration continue est accessible ici, qui crée le code après chaque fusion.
Morpheus est publié sous la licence Apache Software Foundation version 2.