├── .gitignore ├── .styleci.yml ├── README.md ├── changelog.md ├── composer.json ├── config └── rubixai_config.php ├── contributing.md ├── license.md ├── phpunit.xml ├── src ├── Exceptions │ └── RubixAiGeneralException.php ├── Facades │ └── RubixAi.php ├── RubixAiService.php └── RubixAiServiceProvider.php └── tests └── Unit ├── CanTrainAnomalyTest.php ├── CanTrainClassification.php ├── CanTrainClusterer.php ├── CanTrainRegression.php ├── CanTrainRegressionSimple.php ├── LConfigTest.php └── TestEloquentModels ├── Apartment.php ├── IrisFlower.php └── csv ├── apartments_1k.csv └── bezdekiris.csv /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | *.cache 4 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # torian257x/ai-php-rubix-wrap-laravel 2 | This package is about Artificial Intelligence, Machine Learning and Pattern Recognition in Laravel. 3 | 4 | The video tutorial with examples on how to use the library can be found here: 5 |


6 | ## Video tutorial 7 | [how to use AI in Laravel](https://youtu.be/0i2npIenj70) ([full playlist](https://www.youtube.com/watch?v=0i2npIenj70&list=PLInLuJxdnhE-qZJHhhQka0osgYz8_Kn6i)) 8 | 9 | 10 |

11 | 12 | ## Example code 13 | Do AI with 2 lines of code: 14 | 15 | ``` 16 | $all_animals = DogsAndCats::all(); 17 | 18 | $report = RubixAi::train($all_animals, 'dog_or_cat'); 19 | ``` 20 | 21 | that's it. 22 | 23 | This creates a file in your laravel storage/ai_rubix/ folder that contains the model. 24 | 25 | This model then will be used to predict: 26 | 27 | ``` 28 | $isDogOrCat = RubixAi::predict($needs_prediction); 29 | 30 | echo $isDogOrCat; //prints ['dog'] 31 | ``` 32 | 33 | ## Installation 34 | 35 | ``` 36 | composer require torian257x/ai-php-rubix-wrap-laravel 37 | ``` 38 | 39 | If there are any issues, please have a look at https://docs.rubixml.com/latest/installation.html in case you are trying to do something special. I recommend as well installing tensor https://github.com/Scien-ide/Tensor . 40 | 41 | ## Default Estimator 42 | `new KDNeighborsRegressor()` for regression (say estimation of price) 43 | 44 | `new KDNeighbors()` for classification (say choose dog or cat) 45 | 46 | ## Default Transformers 47 | ``` 48 | array_filter( 49 | [ 50 | new NumericStringConverter(), 51 | new MissingDataImputer(), 52 | $needs_ohe ? new OneHotEncoder() : false, 53 | new MinMaxNormalizer(), 54 | ]); 55 | ``` 56 | 57 | ## Customize 58 | You can [choose your own estimator](https://docs.rubixml.com/latest/choosing-an-estimator.html) if you don't like the default 59 | Just be sure to pass it as argument to `RubixAi::train(..., estimator_algorithm:<>)` 60 | 61 | Same for transformers. 62 | 63 | You can customize the model file name as well as what attributes / columns to ignore. 64 | 65 | 66 | 67 | ## References 68 | This library is using a [PHP standalone AI wrapper](https://github.com/torian257x/ai-php-rubix-wrap) of [Rubix ML](https://github.com/RubixML/ML) 69 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `RubixAi` will be documented in this file. 4 | 5 | ## Version 1.0 6 | 7 | ### Added 8 | - Everything 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torian257x/ai-php-rubix-wrap-laravel", 3 | "description": "Meant for AI using a wrapped Rubix ML library to make it very approachable", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Lukas Meier", 8 | "email": "torian257x@gmail.com" 9 | } 10 | ], 11 | "homepage": "https://github.com/torian257x/rubixai", 12 | "keywords": ["Laravel", "RubixAi"], 13 | "require": { 14 | "illuminate/support": "~7|~8", 15 | "calebporzio/sushi": "^2.2", 16 | "torian257x/ai-php-rubix-wrap": "^0.9" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "~9.0", 20 | "orchestra/testbench": "~5|~6" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Torian257x\\RubixAi\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Torian257x\\RubixAi\\Tests\\": "tests" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "Torian257x\\RubixAi\\RubixAiServiceProvider" 36 | ], 37 | "aliases": { 38 | "RubixAi": "Torian257x\\RubixAi\\Facades\\RubixAi" 39 | } 40 | } 41 | }, 42 | "repositories": { 43 | "torian257x/ai-php-rubix-wrap": { 44 | "type": "path", 45 | "url": "/home/jossnaz/IdeaProjects/rubix-wrap", 46 | "options": { 47 | "symlink": true 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/rubixai_config.php: -------------------------------------------------------------------------------- 1 | storage_path('ai_rubix/csv/'), 7 | 'csv_path_input' => storage_path('ai_rubix/csv/'), 8 | 'ai_model_path_output' => storage_path('ai_rubix/model/'), 9 | 'RubixMainClass' => RubixService::class, 10 | ]; 11 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/torian257x/rubixai). 6 | 7 | # Things you could do 8 | If you want to contribute but do not know where to start, this list provides some starting points. 9 | - Add license text 10 | - Remove rewriteRules.php 11 | - Set up TravisCI, StyleCI, ScrutinizerCI 12 | - Write a comprehensive ReadMe 13 | 14 | ## Pull Requests 15 | 16 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 17 | 18 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 19 | 20 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 25 | 26 | 27 | **Happy coding**! 28 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The license 2 | 3 | Copyright (c) Lukas Meier 4 | 5 | ...Add your license text here... -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | 17 | 18 | 19 | src 20 | 21 | src/Examples 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Exceptions/RubixAiGeneralException.php: -------------------------------------------------------------------------------- 1 | toArray(); 92 | } else if ($data instanceof \iterable){ 93 | $rv = iterator_to_array($data); 94 | }else{ 95 | throw new RubixAiGeneralException("Cannot convert this data type to array: " . get_class($data)); 96 | } 97 | 98 | if(!is_null($ignore_attrs)){ 99 | foreach($rv as &$v){ 100 | foreach($ignore_attrs as $i){ 101 | unset($v[$i]); 102 | } 103 | } 104 | } 105 | 106 | return $rv; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/RubixAiServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 18 | $this->bootForConsole(); 19 | } 20 | } 21 | 22 | /** 23 | * Register any package services. 24 | * 25 | * @return void 26 | */ 27 | public function register(): void 28 | { 29 | $this->mergeConfigFrom(__DIR__ . '/../config/rubixai_config.php', 'rubixai'); 30 | if(!defined('RUBIXAI_CUSTOM_CONFIG')){ 31 | define('RUBIXAI_CUSTOM_CONFIG', config('rubixai')); 32 | } 33 | 34 | $this->app->singleton('rubixai', function ($app) { 35 | return new RubixAiService; 36 | }); 37 | } 38 | 39 | /** 40 | * Get the services provided by the provider. 41 | * 42 | * @return array 43 | */ 44 | public function provides() 45 | { 46 | return ['rubixai']; 47 | } 48 | 49 | /** 50 | * Console-specific booting. 51 | * 52 | * @return void 53 | */ 54 | protected function bootForConsole(): void 55 | { 56 | // Publishing the configuration file. 57 | $this->publishes([ 58 | __DIR__ . '/../config/rubixai_config.php' => config_path('rubixai.php'), 59 | ], 'rubixai.config'); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Unit/CanTrainAnomalyTest.php: -------------------------------------------------------------------------------- 1 | get(); 28 | 29 | $apartments = $apartments->map(function($a){ 30 | 31 | if($a->water_heating === 'No tiene'){ 32 | $a->water_heating = 0; 33 | }else if($a->water_heating === 'Gas'){ 34 | $a->water_heating = 1; 35 | }else if($a->water_heating === 'Eléctrico'){ 36 | $a->water_heating = 0.5; 37 | }else{ 38 | $a->water_heating = 0; 39 | } 40 | 41 | if($a->doorman === '24 Horas'){ 42 | $a->doorman = 1; 43 | }else if($a->doorman === 'Diurna'){ 44 | $a->doorman = 0.5; 45 | }else if($a->doorman === 'No tiene'){ 46 | $a->doorman = 0; 47 | }else{ 48 | $a->doorman = 0; 49 | } 50 | 51 | return $a; 52 | 53 | }); 54 | 55 | $apartments->makeHidden(['zone_2_id', 'zone_id']); 56 | 57 | $data = RubixAi::trainWithoutTest($apartments, estimator_algorithm: new GaussianMLE(contamination: 0.005)); 58 | 59 | 60 | 61 | RubixAi::toCsv($data, 'cluster_out.csv'); 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /tests/Unit/CanTrainClassification.php: -------------------------------------------------------------------------------- 1 | get(); 38 | $data = $data->map( 39 | function (Apartment $a) { 40 | 41 | $wh = $a->water_heating; 42 | 43 | if ($wh === 'No tiene') { 44 | $wh_val = 0; 45 | } else if ($wh === 'Gas') { 46 | $wh_val = 1; 47 | } else if ($wh === 'Eléctrico') { 48 | $wh_val = 0.5; 49 | } else { 50 | $wh_val = 0; 51 | } 52 | $a->water_heating = $wh_val; 53 | 54 | $dm = $a->doorman; 55 | 56 | 57 | if ($dm === '24 Horas') { 58 | $dm_val = 1; 59 | } else if ($dm === 'Diurna') { 60 | $dm_val = 0.5; 61 | } else if ($dm === 'No tiene') { 62 | $dm_val = 0; 63 | } else { 64 | $dm_val = 0; 65 | } 66 | 67 | $a->doorman = $dm_val; 68 | 69 | $a->rr = pow($a->rooms, 2); 70 | $a->pp = pow($a->price_millions, 2); 71 | $a->pp3 = pow($a->price_millions, 3); 72 | $a->p_t_r = $a->price_millions * $a->rooms ; 73 | $a->p_t_lat = $a->price_millions * $a->geo_lat; 74 | $a->p_t_lng = $a->price_millions * $a->geo_lng; 75 | $a->latlng1 = $a->geo_lat * $a->geo_lng; 76 | $a->latlng2 = pow($a->geo_lat * $a->geo_lng, 2); 77 | 78 | $rv = $a->toArray(); 79 | 80 | unset($rv['zone_id']); 81 | unset($rv['zone_2_id']); 82 | 83 | return $rv; 84 | } 85 | ); 86 | 87 | 88 | return $data; 89 | } 90 | 91 | public function testGetSimilar() 92 | { 93 | 94 | $data = self::getData(); 95 | 96 | $nr_groups = ceil(sqrt(count($data) / 2)); 97 | 98 | 99 | $data_w_cluster_nr = RubixAi::trainWithoutTest( 100 | $data, 101 | estimator_algorithm: new KMeans($nr_groups, kernel: new Manhattan()), 102 | transformers: [ 103 | new MissingDataImputer(), 104 | new NumericStringConverter(), 105 | new ZScaleStandardizer(), 106 | ] 107 | ); 108 | 109 | $clusters = array_column($data_w_cluster_nr,'cluster_nr'); 110 | $sum_clusters = array_sum($clusters); 111 | 112 | 113 | self::assertGreaterThan(100, $sum_clusters); 114 | 115 | 116 | } 117 | 118 | } 119 | 120 | -------------------------------------------------------------------------------- /tests/Unit/CanTrainRegression.php: -------------------------------------------------------------------------------- 1 | get(); 27 | 28 | $report = RubixAi::train($apartments, 'price_millions', train_part_size: 0.95); 29 | 30 | self::assertLessThan(0.4, $report['mean absolute error']); 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/CanTrainRegressionSimple.php: -------------------------------------------------------------------------------- 1 | 10, 'price' => 100], 27 | ['space_m2' => 20, 'price' => 200], 28 | ['space_m2' => 30, 'price' => 300], 29 | ['space_m2' => 40, 'price' => 400], 30 | ['space_m2' => 50, 'price' => 500], 31 | ['space_m2' => 60, 'price' => 600], 32 | ['space_m2' => 70, 'price' => 700], 33 | ['space_m2' => 80, 'price' => 800], 34 | ['space_m2' => 90, 'price' => 900], 35 | ['space_m2' => 100, 'price' => 1000], 36 | ['space_m2' => 110, 'price' => 1100], 37 | ['space_m2' => 120, 'price' => 1200], 38 | ['space_m2' => 130, 'price' => 1300], 39 | ['space_m2' => 140, 'price' => 1400], 40 | ['space_m2' => 150, 'price' => 1500], 41 | ['space_m2' => 160, 'price' => 1600], 42 | ['space_m2' => 170, 'price' => 1700], 43 | ['space_m2' => 180, 'price' => 1800], 44 | ['space_m2' => 190, 'price' => 1900], 45 | ['space_m2' => 200, 'price' => 2000], 46 | ['space_m2' => 210, 'price' => 2100], 47 | ['space_m2' => 220, 'price' => 2200], 48 | ['space_m2' => 230, 'price' => 2300], 49 | ['space_m2' => 240, 'price' => 2400], 50 | ['space_m2' => 250, 'price' => 2500], 51 | ['space_m2' => 260, 'price' => 2600], 52 | ['space_m2' => 270, 'price' => 2700], 53 | ['space_m2' => 280, 'price' => 2800], 54 | ['space_m2' => 290, 'price' => 2900], 55 | ['space_m2' => 300, 'price' => 3000], 56 | ]; 57 | 58 | 59 | $report = RubixAi::train($apartment_data, 'price',); 60 | 61 | self::assertGreaterThan(0.9, $report['r squared']); 62 | 63 | 64 | $prediction = RubixAi::predict(['space_m2' => 250]); 65 | 66 | self::assertLessThan(0.1 * 2500, abs($prediction - 2500)); 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /tests/Unit/LConfigTest.php: -------------------------------------------------------------------------------- 1 | 'float', 15 | 'price_millions' => 'float', 16 | 'space' => 'int', 17 | 'rooms' => 'int', 18 | 'zone_id' => 'string', 19 | 'zone_2_id' => 'string', 20 | 'geo_lat' => 'float', 21 | 'geo_lng' => 'float', 22 | 'parking' => 'int', 23 | 'water_heating' => 'string', 24 | 'doorman' => 'string', 25 | 'price_level' => 'int', 26 | 'gas_network' => 'string', 27 | 'balcony' => 'int', 28 | 'linen_room' => 'int', 29 | ]; 30 | 31 | public function getRows() 32 | { 33 | $data = new CSV(__DIR__ .'/csv/apartments_1k.csv', true); 34 | $rows = iterator_to_array($data); 35 | return $rows; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Unit/TestEloquentModels/IrisFlower.php: -------------------------------------------------------------------------------- 1 | 'float', 15 | 'sepal_width_cm' => 'float', 16 | 'petal_length_cm' => 'float', 17 | 'petal_width_cm' => 'float', 18 | 'iris_plant_type' => 'string', 19 | ]; 20 | 21 | public function getRows() 22 | { 23 | $data = new CSV(__DIR__ .'/csv/bezdekiris.csv', true); 24 | $rows = iterator_to_array($data); 25 | return $rows; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Unit/TestEloquentModels/csv/bezdekiris.csv: -------------------------------------------------------------------------------- 1 | sepal_length_cm,sepal_width_cm,petal_length_cm,petal_width_cm,iris_plant_type 2 | 5.1,3.5,1.4,0.2,setosa 3 | 4.9,3,1.4,0.2,setosa 4 | 4.7,3.2,1.3,0.2,setosa 5 | 4.6,3.1,1.5,0.2,setosa 6 | 5,3.6,1.4,0.2,setosa 7 | 5.4,3.9,1.7,0.4,setosa 8 | 4.6,3.4,1.4,0.3,setosa 9 | 5,3.4,1.5,0.2,setosa 10 | 4.4,2.9,1.4,0.2,setosa 11 | 4.9,3.1,1.5,0.1,setosa 12 | 5.4,3.7,1.5,0.2,setosa 13 | 4.8,3.4,1.6,0.2,setosa 14 | 4.8,3,1.4,0.1,setosa 15 | 4.3,3,1.1,0.1,setosa 16 | 5.8,4,1.2,0.2,setosa 17 | 5.7,4.4,1.5,0.4,setosa 18 | 5.4,3.9,1.3,0.4,setosa 19 | 5.1,3.5,1.4,0.3,setosa 20 | 5.7,3.8,1.7,0.3,setosa 21 | 5.1,3.8,1.5,0.3,setosa 22 | 5.4,3.4,1.7,0.2,setosa 23 | 5.1,3.7,1.5,0.4,setosa 24 | 4.6,3.6,1,0.2,setosa 25 | 5.1,3.3,1.7,0.5,setosa 26 | 4.8,3.4,1.9,0.2,setosa 27 | 5,3,1.6,0.2,setosa 28 | 5,3.4,1.6,0.4,setosa 29 | 5.2,3.5,1.5,0.2,setosa 30 | 5.2,3.4,1.4,0.2,setosa 31 | 4.7,3.2,1.6,0.2,setosa 32 | 4.8,3.1,1.6,0.2,setosa 33 | 5.4,3.4,1.5,0.4,setosa 34 | 5.2,4.1,1.5,0.1,setosa 35 | 5.5,4.2,1.4,0.2,setosa 36 | 4.9,3.1,1.5,0.2,setosa 37 | 5,3.2,1.2,0.2,setosa 38 | 5.5,3.5,1.3,0.2,setosa 39 | 4.9,3.6,1.4,0.1,setosa 40 | 4.4,3,1.3,0.2,setosa 41 | 5.1,3.4,1.5,0.2,setosa 42 | 5,3.5,1.3,0.3,setosa 43 | 4.5,2.3,1.3,0.3,setosa 44 | 4.4,3.2,1.3,0.2,setosa 45 | 5,3.5,1.6,0.6,setosa 46 | 5.1,3.8,1.9,0.4,setosa 47 | 4.8,3,1.4,0.3,setosa 48 | 5.1,3.8,1.6,0.2,setosa 49 | 4.6,3.2,1.4,0.2,setosa 50 | 5.3,3.7,1.5,0.2,setosa 51 | 5,3.3,1.4,0.2,setosa 52 | 7,3.2,4.7,1.4,versicolor 53 | 6.4,3.2,4.5,1.5,versicolor 54 | 6.9,3.1,4.9,1.5,versicolor 55 | 5.5,2.3,4,1.3,versicolor 56 | 6.5,2.8,4.6,1.5,versicolor 57 | 5.7,2.8,4.5,1.3,versicolor 58 | 6.3,3.3,4.7,1.6,versicolor 59 | 4.9,2.4,3.3,1,versicolor 60 | 6.6,2.9,4.6,1.3,versicolor 61 | 5.2,2.7,3.9,1.4,versicolor 62 | 5,2,3.5,1,versicolor 63 | 5.9,3,4.2,1.5,versicolor 64 | 6,2.2,4,1,versicolor 65 | 6.1,2.9,4.7,1.4,versicolor 66 | 5.6,2.9,3.6,1.3,versicolor 67 | 6.7,3.1,4.4,1.4,versicolor 68 | 5.6,3,4.5,1.5,versicolor 69 | 5.8,2.7,4.1,1,versicolor 70 | 6.2,2.2,4.5,1.5,versicolor 71 | 5.6,2.5,3.9,1.1,versicolor 72 | 5.9,3.2,4.8,1.8,versicolor 73 | 6.1,2.8,4,1.3,versicolor 74 | 6.3,2.5,4.9,1.5,versicolor 75 | 6.1,2.8,4.7,1.2,versicolor 76 | 6.4,2.9,4.3,1.3,versicolor 77 | 6.6,3,4.4,1.4,versicolor 78 | 6.8,2.8,4.8,1.4,versicolor 79 | 6.7,3,5,1.7,versicolor 80 | 6,2.9,4.5,1.5,versicolor 81 | 5.7,2.6,3.5,1,versicolor 82 | 5.5,2.4,3.8,1.1,versicolor 83 | 5.5,2.4,3.7,1,versicolor 84 | 5.8,2.7,3.9,1.2,versicolor 85 | 6,2.7,5.1,1.6,versicolor 86 | 5.4,3,4.5,1.5,versicolor 87 | 6,3.4,4.5,1.6,versicolor 88 | 6.7,3.1,4.7,1.5,versicolor 89 | 6.3,2.3,4.4,1.3,versicolor 90 | 5.6,3,4.1,1.3,versicolor 91 | 5.5,2.5,4,1.3,versicolor 92 | 5.5,2.6,4.4,1.2,versicolor 93 | 6.1,3,4.6,1.4,versicolor 94 | 5.8,2.6,4,1.2,versicolor 95 | 5,2.3,3.3,1,versicolor 96 | 5.6,2.7,4.2,1.3,versicolor 97 | 5.7,3,4.2,1.2,versicolor 98 | 5.7,2.9,4.2,1.3,versicolor 99 | 6.2,2.9,4.3,1.3,versicolor 100 | 5.1,2.5,3,1.1,versicolor 101 | 5.7,2.8,4.1,1.3,versicolor 102 | 6.3,3.3,6,2.5,virginica 103 | 5.8,2.7,5.1,1.9,virginica 104 | 7.1,3,5.9,2.1,virginica 105 | 6.3,2.9,5.6,1.8,virginica 106 | 6.5,3,5.8,2.2,virginica 107 | 7.6,3,6.6,2.1,virginica 108 | 4.9,2.5,4.5,1.7,virginica 109 | 7.3,2.9,6.3,1.8,virginica 110 | 6.7,2.5,5.8,1.8,virginica 111 | 7.2,3.6,6.1,2.5,virginica 112 | 6.5,3.2,5.1,2,virginica 113 | 6.4,2.7,5.3,1.9,virginica 114 | 6.8,3,5.5,2.1,virginica 115 | 5.7,2.5,5,2,virginica 116 | 5.8,2.8,5.1,2.4,virginica 117 | 6.4,3.2,5.3,2.3,virginica 118 | 6.5,3,5.5,1.8,virginica 119 | 7.7,3.8,6.7,2.2,virginica 120 | 7.7,2.6,6.9,2.3,virginica 121 | 6,2.2,5,1.5,virginica 122 | 6.9,3.2,5.7,2.3,virginica 123 | 5.6,2.8,4.9,2,virginica 124 | 7.7,2.8,6.7,2,virginica 125 | 6.3,2.7,4.9,1.8,virginica 126 | 6.7,3.3,5.7,2.1,virginica 127 | 7.2,3.2,6,1.8,virginica 128 | 6.2,2.8,4.8,1.8,virginica 129 | 6.1,3,4.9,1.8,virginica 130 | 6.4,2.8,5.6,2.1,virginica 131 | 7.2,3,5.8,1.6,virginica 132 | 7.4,2.8,6.1,1.9,virginica 133 | 7.9,3.8,6.4,2,virginica 134 | 6.4,2.8,5.6,2.2,virginica 135 | 6.3,2.8,5.1,1.5,virginica 136 | 6.1,2.6,5.6,1.4,virginica 137 | 7.7,3,6.1,2.3,virginica 138 | 6.3,3.4,5.6,2.4,virginica 139 | 6.4,3.1,5.5,1.8,virginica 140 | 6,3,4.8,1.8,virginica 141 | 6.9,3.1,5.4,2.1,virginica 142 | 6.7,3.1,5.6,2.4,virginica 143 | 6.9,3.1,5.1,2.3,virginica 144 | 5.8,2.7,5.1,1.9,virginica 145 | 6.8,3.2,5.9,2.3,virginica 146 | 6.7,3.3,5.7,2.5,virginica 147 | 6.7,3,5.2,2.3,virginica 148 | 6.3,2.5,5,1.9,virginica 149 | 6.5,3,5.2,2,virginica 150 | 6.2,3.4,5.4,2.3,virginica 151 | 5.9,3,5.1,1.8,virginica 152 | --------------------------------------------------------------------------------