From 16a024290606353213979210fb8cb82a2555623a Mon Sep 17 00:00:00 2001 From: sarahmish Date: Mon, 8 Feb 2021 16:26:31 -0500 Subject: [PATCH] update readme and docs --- .github/workflows/tests.yml | 30 +- Makefile | 11 +- README.md | 187 ++++++++-- cardea/__init__.py | 2 +- cardea/cardea.py | 239 ------------ cardea/core.py | 348 ++++++++++++++++++ cardea/problem_definition/__init__.py | 4 +- .../show_noshow_appointment.py | 2 +- docs/api_reference/cardea.rst | 13 +- docs/api_reference/modeling.rst | 8 +- docs/api_reference/problem_definition.rst | 4 +- docs/authors.rst | 1 - docs/basic_concepts/index.rst | 2 + .../basic_concepts/machine_learning_tasks.rst | 4 +- docs/community/contributing.rst | 6 +- docs/getting_started/quickstart.rst | 67 ++-- docs/images/cardea-process.png | Bin 0 -> 71017 bytes docs/index.rst | 43 +-- setup.py | 20 +- .../test_show_noshow_appointment.py | 22 +- tox.ini | 4 +- 21 files changed, 643 insertions(+), 374 deletions(-) delete mode 100644 cardea/cardea.py create mode 100644 cardea/core.py delete mode 100644 docs/authors.rst create mode 100644 docs/images/cardea-process.png diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98cc6f1f..24f43a7f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,6 +7,23 @@ on: branches: [ master ] jobs: + devel: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.6, 3.7] + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install package + run: pip install .[dev] + - name: make test-devel + run: make test-devel + unit: runs-on: ${{ matrix.os }} strategy: @@ -24,7 +41,7 @@ jobs: - name: Test with pytest run: make test - devel: + readme: runs-on: ${{ matrix.os }} strategy: matrix: @@ -36,9 +53,8 @@ jobs: uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - - name: Install package - run: pip install .[dev] - - name: Lint with flake8 - run: make lint - - name: Test with pytest - run: make test + - name: Install package and dependencies + run: pip install rundoc . + - name: make test-readme + run: make test-readme + diff --git a/Makefile b/Makefile index 683d65bd..e7f11bd4 100644 --- a/Makefile +++ b/Makefile @@ -115,8 +115,17 @@ test: ## run tests quickly with the default Python test-all: ## run tests on every Python version with tox tox +.PHONY: test-readme +test-readme: ## run the readme snippets + rundoc run --single-session python3 -t python3 README.md +.PHONY: check-dependencies +check-dependencies: ## test if there are any broken dependencies + pip check + +.PHONY: test-devel +test-devel: check-dependencies lint docs ## test everything that needs development dependencies .PHONY: coverage @@ -131,9 +140,7 @@ coverage: clean-coverage ## check code coverage quickly with the default Python .PHONY: docs docs: clean-docs ## generate Sphinx HTML documentation, including API docs - sphinx-apidoc --module-first --separate --no-toc --output-dir docs/api/ cardea $(MAKE) -C docs html - touch docs/_build/html/.nojekyll .PHONY: viewdocs viewdocs: ## view the docs in a browser diff --git a/README.md b/README.md index 2fb80d25..22a1a54c 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,6 @@ “Cardea”

-

-Cardea is a machine learning library built on top of FHIR schema. -

- -

-An open source project from Data to AI Lab at MIT -

- - [![Development Status](https://img.shields.io/badge/Development%20Status-2%20--%20Pre--Alpha-yellow)](https://pypi.org/search/?c=Development+Status+%3A%3A+2+-+Pre-Alpha) [![PyPi Shield](https://img.shields.io/pypi/v/cardea.svg)](https://pypi.python.org/pypi/cardea) @@ -19,24 +10,168 @@ # Cardea -This library is under development. Please contact dai-lab@mit.edu or any of the contributors for more information. We will announce our first release soon. +*This library is under development. Please contact dai-lab@mit.edu or any of the contributors for more information.* + +* License: [MIT](https://github.com/MLBazaar/Cardea/blob/master/LICENSE) +* Development Status: [Pre-Alpha](https://pypi.org/search/?c=Development+Status+%3A%3A+2+-+Pre-Alpha) +* Homepage: https://github.com/MLBazaar/Cardea +* Documentation: https://MLBazaar.github.io/Cardea + +# Overview + +Cardea is a machine learning library built on top of *schemas* that support electronic health records (EHR). The library uses a number of AutoML tools developed under [The Human Data Interaction Project](https://github.com/HDI-Project) at [Data to AI Lab at MIT](https://dai.lids.mit.edu/). + + +Our goal is to provide an easy to use library to develop machine learning models from electronic health records. A typical usage of this library will involve interacting with our API to develop prediction models. + + ![process](docs/images/cardea-process.png) + +A series of sequential processes are applied to build a machine learning model. These processes are triggered using our following APIs to perform the following: + +* loading data using the automatic **data assembler**, where we capture data from its raw format into an entityset representation. + +* **data labeling** where we create label times that generates (1) the time index that indicates the timespan for which I create my features (2) the encoded labels of the prediction task. this is essential for our feature engineering phase. + +* **featurization** for which we automatically feature engineer our data to generate a feature matrix. + +* lastly, we build, train, and tune our machine learning model using the **modeling** component. + +to learn more about how we structure our machine learning process and our data structures, read our documentation [here](https://MLBazaar.github.io/Cardea). + +# Quickstart + +## Install with pip + + +The easiest and recommended way to install **Cardea** is using [pip](https://pip.pypa.io/en/stable/): + +```bash +pip install cardea +``` + +This will pull and install the latest stable release from [PyPi](https://pypi.org/). + +## Quickstart + +In this short tutorial we will guide you through a series of steps that will help you get Cardea started. + +First, load the core class to work with: + +```python3 +from cardea import Cardea + +cardea = Cardea() +``` + +We then seamlessly plug in our data. Here in this example, we are loading a pre-processed version of the [Kaggle dataset: Medical Appointment No Shows](https://www.kaggle.com/joniarroba/noshowappointments). +To use this dataset download the data from here then unzip it in the root directory, or run the command: + +```bash +curl -O https://dai-cardea.s3.amazonaws.com/kaggle.zip && unzip kaggle.zip +``` +To load the data, supply the ``folder_path`` to the loader using the following command: + +```python3 +cardea.load_entityset() +``` +> :bulb: To load local data, use ``cardea.load_entityset(folder_path='kaggle')``. + +To verify that the data has been loaded, you can find the loaded entityset by viewing ``cardea.es`` which should output the following: + +```bash +Entityset: kaggle + Entities: + Address [Rows: 81, Columns: 2] + Appointment_Participant [Rows: 6100, Columns: 2] + Appointment [Rows: 110527, Columns: 5] + CodeableConcept [Rows: 4, Columns: 2] + Coding [Rows: 3, Columns: 2] + Identifier [Rows: 227151, Columns: 1] + Observation [Rows: 110527, Columns: 3] + Patient [Rows: 6100, Columns: 4] + Reference [Rows: 6100, Columns: 1] + Relationships: + Appointment_Participant.actor -> Reference.identifier + Appointment.participant -> Appointment_Participant.object_id + CodeableConcept.coding -> Coding.object_id + Observation.code -> CodeableConcept.object_id + Observation.subject -> Reference.identifier + Patient.address -> Address.object_id +``` + +The output shown represents the entityset data structure where ``cardea.es`` is composed of entities and relationships. You can read more about entitysets [here](https://mlbazaar.github.io/Cardea/basic_concepts/data_loading.html). + +From there, you can select the prediction problem you aim to solve by specifying the name of the class, which in return gives us the ``label_times`` of the problem. + +```python3 +label_times = cardea.select_problem('MissedAppointment') +``` + +``label_times`` summarizes for each instance in the dataset (1) what is its corresponding label of the instance and (2) what is the time index that indicates the timespan allowed for calculating features that pertain to each instance in the dataset. + +```bash + cutoff_time instance_id label +0 2015-11-10 07:13:56 5030230 noshow +1 2015-12-03 08:17:28 5122866 fulfilled +2 2015-12-07 10:40:59 5134197 fulfilled +3 2015-12-07 10:42:42 5134220 noshow +4 2015-12-07 10:43:01 5134223 noshow +``` + +You can read more about ``label_times`` [here](https://mlbazaar.github.io/Cardea/basic_concepts/machine_learning_tasks.html). + +Then, you can perform the AutoML steps and take advantage of Cardea. + +Cardea extracts features through automated feature engineering by supplying the ``label_times`` pertaining to the problem you aim to solve + +```python3 +feature_matrix = cardea.generate_features(label_times[:1000]) +``` +> :warning: Featurizing the data might take a while depending on the size of the data. For demonstration, we only featurize the first 1000 records. + +Once we have the features, we can now split the data into training and testing + +```python3 +y = list(feature_matrix.pop('label')) + +X = feature_matrix.values + +X_train, X_test, y_train, y_test = cardea.train_test_split( + X, y, test_size=0.2, shuffle=True) +``` + +Now that we have our feature matrix properly divided, we can use to train our machine learning pipeline, Modeling, optimizing hyperparameters and finding the most optimal model + +```python3 +cardea.select_pipeline('Random Forest') +cardea.fit(X_train, y_train) +y_pred = cardea.predict(X_test) +``` -Cardea is a machine learning library built on top of the FHIR data schema. The library uses a number of automl tools developed under ["The Human Data Interaction Project"](https://github.com/HDI-Project) at [Data to AI lab at MIT](https://dai.lids.mit.edu/). Our goal is to provide an easy to use library to develop machine learning models from electronic health records. A typical usage of this library will involve: +Finally, you can evaluate the performance of the model +```python3 +cardea.evaluate(X, y, test_size=0.2, shuffle=True) +``` +which returns the scoring metric depending on the type of problem +```bash +{'Accuracy': 0.75, + 'F1 Macro': 0.5098039215686274, + 'Precision': 0.5183001719479243, + 'Recall': 0.5123528436411872} +``` -* Installing the library available via pypi -* Integrating their data in FHIR schema (whatever subset of data is available) -* Following the API develop some pre specified prediction models (or specify new ones using our API) The model building process is parameterized but automatically does: - * data cleaning, auditing - * preprocessing - * feature engineering - * machine learning model search and tuning - * model evaluation - * model auditing -* Testing the models using our API -* Preparing and deploying the models +# Citation +If you use Cardea for your research, please consider citing the following paper: -## License -- Free software: MIT license +Sarah Alnegheimish; Najat Alrashed; Faisal Aleissa; Shahad Althobaiti; Dongyu Liu; Mansour Alsaleh; Kalyan Veeramachaneni. [Cardea: An Open Automated Machine Learning Framework for Electronic Health Records](https://arxiv.org/abs/2010.00509). [IEEE DSAA 2020](https://ieeexplore.ieee.org/document/9260104). -## Documentation -- Documentation: https://mlbazaar.github.io/Cardea +```bash +@inproceedings{alnegheimish2020cardea, + title={Cardea: An Open Automated Machine Learning Framework for Electronic Health Records}, + author={Alnegheimish, Sarah and Alrashed, Najat and Aleissa, Faisal and Althobaiti, Shahad and Liu, Dongyu and Alsaleh, Mansour and Veeramachaneni, Kalyan}, + booktitle={2020 IEEE 7th International Conference on Data Science and Advanced Analytics (DSAA)}, + pages={536--545}, + year={2020}, + organization={IEEE} +} +``` diff --git a/cardea/__init__.py b/cardea/__init__.py index 7e1c0fbf..18c15abf 100644 --- a/cardea/__init__.py +++ b/cardea/__init__.py @@ -8,7 +8,7 @@ import logging import os -from cardea.cardea import Cardea +from cardea.core import Cardea logging.getLogger('cardea').addHandler(logging.NullHandler()) diff --git a/cardea/cardea.py b/cardea/cardea.py deleted file mode 100644 index b239fb9b..00000000 --- a/cardea/cardea.py +++ /dev/null @@ -1,239 +0,0 @@ -import json -from inspect import isclass - -import featuretools as ft -import pandas as pd - -import cardea -from cardea.data_loader import EntitySetLoader -from cardea.featurization import Featurization -from cardea.modeling import Modeler -from cardea.problem_definition import ( - DiagnosisPrediction, LengthOfStay, MissedAppointmentProblemDefinition, MortalityPrediction, - ProlongedLengthOfStay, Readmission) - - -class Cardea(): - """An interface class that ties the end-to-end system together. - - Args: - es_loader (EntitySetLoader): - An entityset loader. - featurization (Featurization): - A featurization class. - modeler (Modeler): - A modeling class. - problems (list): - A list of currently available prediction problems. - chosen_problem (str): - The selected prediction problem or regression. - es (featuretools.EntitySet): - The loaded entityset. - target_entity (str): - The target entity for featurization. - """ - - def __init__(self): - - self.es_loader = EntitySetLoader() - self.featurization = Featurization() - self.modeler = Modeler() - - self.es = None - self.chosen_problem = None - self.target_entity = None - - def load_data_entityset(self, folder_path=None): - """Returns an entityset loaded with .csv files in folder_path. - - Load the given dataset within the folder path into an entityset. The dataset - must be in a FHIR structure format. If no folder_path is not passed, the - function will automatically load kaggle's missed appointment dataset. - - Args: - folder_path (str): - A directory of all .csv files that should be loaded. - - Returns: - featuretools.EntitySet: - An entityset with loaded data. - """ - - if folder_path: - self.es = self.es_loader.load_data_entityset(folder_path) - - else: - csv_s3 = "https://s3.amazonaws.com/dai-cardea/" - kaggle = ['Address', - 'Appointment_Participant', - 'Appointment', - 'CodeableConcept', - 'Coding', - 'Identifier', - 'Observation', - 'Patient', - 'Reference'] - - fhir = { - resource: pd.read_csv( - csv_s3 + resource + ".csv") for resource in kaggle} - self.es = self.es_loader.load_df_entityset(fhir) - - def list_problems(self): - """Returns a list of the currently available problems. - - Returns: - list: - A list of the available problems. - """ - - problems = set([]) - for attribute_string in dir(cardea.problem_definition): - attribute = getattr(cardea.problem_definition, attribute_string) - if isclass(attribute): - if attribute.__name__ and attribute.__name__ != 'ProblemDefinition': - problems.add(attribute.__name__) - - return problems - - def select_problem(self, selection, parameter=None): - """Select a prediction problem and extract information. - - Update the select_problem attribute and generate the cutoff times, - the target entity and update the entityset. - - Args: - selection (str): - Name of the chosen prediction problem. - parameters (dict): - Variables to change the default parameters, if any. - - Returns: - featuretools.EntitySet, str, pandas.DataFrame: - * An updated EntitySet if a new column is generated. - * A string indicating the selected target entity. - * A dataframe of cutoff times and their target labels. - """ - - # problem selection - if selection == 'LengthOfStay': - self.chosen_problem = LengthOfStay() - - elif selection == 'MortalityPrediction': - self.chosen_problem = MortalityPrediction() - - elif selection == 'MissedAppointmentProblemDefinition': - self.chosen_problem = MissedAppointmentProblemDefinition() - - elif selection == 'ProlongedLengthOfStay' and parameter: - self.chosen_problem = ProlongedLengthOfStay(parameter) - - elif selection == 'ProlongedLengthOfStay': - self.chosen_problem = ProlongedLengthOfStay() - - elif selection == 'Readmission' and parameter: - self.chosen_problem = Readmission(parameter) - - elif selection == 'Readmission': - self.chosen_problem = Readmission() - - elif selection == 'DiagnosisPrediction' and parameter: - self.chosen_problem = DiagnosisPrediction(parameter) - - elif selection == 'DiagnosisPrediction': - raise ValueError('unspecified diagnosis code') - - else: - raise ValueError('{} is not a defined problem'.format(selection)) - - # target label calculation - self.es, self.target_entity, cutoff = self.chosen_problem.generate_cutoff_times(self.es) - return cutoff - - def list_feature_primitives(self): - """Returns built-in primitive in Featuretools. - - Returns: - pandas.DataFrame: - A dataframe that lists and describes each built-in primitives. - """ - return ft.list_primitives() - - def generate_features(self, cutoff): - """Returns a the calculated feature matrix. - - Args: - es (featuretools.EntitySet): - An entityset that holds data. - cutoff (pandas.DataFrame): - A dataframe that indicates cutoff time for each instance. - - Returns: - pandas.DataFrame, list: - * The generated feature matrix. - * List of feature definitions in the feature matrix. - """ - - fm_encoded, _ = self.featurization.generate_feature_matrix( - self.es, self.target_entity, cutoff) - fm_encoded = fm_encoded.reset_index(drop=True) - return fm_encoded - - def execute_model(self, feature_matrix, target, primitives, - optimize=False, hyperparameters=None): - """Executes and predicts all of the pipelines. - - This method executes the given pipeline and returns a list for all the pipelines - with the result of each fold with its associated predicted values and actual values. - - Args: - data_frame (pandas.DataFrame or ndarray): - A dataframe which encapsulates all the feature matrix. - target (ndarray): - An array of labels for the target variable. - primitives_list (list): - A list of the primitives within a pipeline. - optimize (bool): - A boolean value which indicates whether to optimize the model or not. - hyperparameters (dict): - A dictionary of hyperparameters for each primitive. - - Returns: - dict: - A dictionary for all the executed pipelines and its result. - """ - - return self.modeler.execute_pipeline( - data_frame=feature_matrix, - target=target, - primitives_list=primitives, - problem_type=self.chosen_problem.prediction_type, - optimize=optimize, - hyperparameters=hyperparameters - ) - - def convert_to_json(X): - """Converts a given dictionary to json format. - - Args: - X (dict): - A dictionary of values to be coverted. - - Returns: - str: - A string in json format. - """ - return json.dumps(X) - - def convert_from_json(X): - """Converts a given json string to dictionary format. - - Args: - X (str): - A string of values to be coverted to json. - - Returns: - dict: - A parsed dictionary. - """ - return json.loads(X) diff --git a/cardea/core.py b/cardea/core.py new file mode 100644 index 00000000..b884a205 --- /dev/null +++ b/cardea/core.py @@ -0,0 +1,348 @@ +"""Cardea Core module. + +This module defines the Cardea Class, which is responsible for the +tying all components together, as well as the interact with them. +""" + +import logging +import os +import pickle +from inspect import isclass + +import featuretools as ft +import pandas as pd + +import cardea +from cardea.data_loader import EntitySetLoader +from cardea.featurization import Featurization +from cardea.modeling import Modeler +from cardea.problem_definition import ( + DiagnosisPrediction, LengthOfStay, MissedAppointment, MortalityPrediction, + ProlongedLengthOfStay, Readmission) + +LOGGER = logging.getLogger(__name__) + + +class Cardea(): + """An interface class that ties the end-to-end system together. + + Args: + es_loader (EntitySetLoader): + An entityset loader. + featurization (Featurization): + A featurization class. + modeler (Modeler): + A modeling class. + problems (list): + A list of currently available prediction problems. + chosen_problem (str): + The selected prediction problem or regression. + es (featuretools.EntitySet): + The loaded entityset. + target_entity (str): + The target entity for featurization. + """ + + def __init__(self): + + self.es_loader = EntitySetLoader() + self.featurization = Featurization() + + self.es = None + self.chosen_problem = None + self.target_entity = None + self.modeler = None + + def load_entityset(self, folder_path=None): + """Returns an entityset loaded with .csv files in folder_path. + + Load the given dataset within the folder path into an entityset. The dataset + must be in a FHIR structure format. If no folder_path is not passed, the + function will automatically load kaggle's missed appointment dataset. + + Args: + folder_path (str): + A directory of all .csv files that should be loaded. + + Returns: + featuretools.EntitySet: + An entityset with loaded data. + """ + + if folder_path: + self.es = self.es_loader.load_data_entityset(folder_path) + + else: + csv_s3 = "https://s3.amazonaws.com/dai-cardea/" + kaggle = ['Address', + 'Appointment_Participant', + 'Appointment', + 'CodeableConcept', + 'Coding', + 'Identifier', + 'Observation', + 'Patient', + 'Reference'] + + fhir = { + resource: pd.read_csv( + csv_s3 + resource + ".csv") for resource in kaggle} + self.es = self.es_loader.load_df_entityset(fhir) + + def list_problems(self): + """Returns a list of the currently available problems. + + Returns: + list: + A list of the available problems. + """ + + problems = set([]) + for attribute_string in dir(cardea.problem_definition): + attribute = getattr(cardea.problem_definition, attribute_string) + if isclass(attribute): + if attribute.__name__ and attribute.__name__ != 'ProblemDefinition': + problems.add(attribute.__name__) + + return problems + + def select_problem(self, selection, parameter=None): + """Select a prediction problem and extract information. + + Update the select_problem attribute and generate the cutoff times, + the target entity and update the entityset. + + Args: + selection (str): + Name of the chosen prediction problem. + parameters (dict): + Variables to change the default parameters, if any. + + Returns: + featuretools.EntitySet, str, pandas.DataFrame: + * An updated EntitySet if a new column is generated. + * A string indicating the selected target entity. + * A dataframe of cutoff times and their target labels. + """ + LOGGER.info("Selecting %s prediction problem", selection) + + # problem selection + if selection == 'LengthOfStay': + self.chosen_problem = LengthOfStay() + + elif selection == 'MortalityPrediction': + self.chosen_problem = MortalityPrediction() + + elif selection == 'MissedAppointment': + self.chosen_problem = MissedAppointment() + + elif selection == 'ProlongedLengthOfStay' and parameter: + self.chosen_problem = ProlongedLengthOfStay(parameter) + + elif selection == 'ProlongedLengthOfStay': + self.chosen_problem = ProlongedLengthOfStay() + + elif selection == 'Readmission' and parameter: + self.chosen_problem = Readmission(parameter) + + elif selection == 'Readmission': + self.chosen_problem = Readmission() + + elif selection == 'DiagnosisPrediction' and parameter: + self.chosen_problem = DiagnosisPrediction(parameter) + + elif selection == 'DiagnosisPrediction': + raise ValueError('unspecified diagnosis code') + + else: + raise ValueError('{} is not a defined problem'.format(selection)) + + # target label calculation + self.es, self.target_entity, cutoff = self.chosen_problem.generate_cutoff_times(self.es) + + # set default pipeline + if self.chosen_problem.prediction_type == "classification": + pipeline = "Random Forest" + else: + pipeline = "Random Forest Regressor" + + self.modeler = Modeler(pipeline, self.chosen_problem.prediction_type) + + return cutoff + + def list_feature_primitives(self): + """Returns built-in primitive in Featuretools. + + Returns: + pandas.DataFrame: + A dataframe that lists and describes each built-in primitives. + """ + return ft.list_primitives() + + def generate_features(self, cutoff): + """Returns a the calculated feature matrix. + + Args: + es (featuretools.EntitySet): + An entityset that holds data. + cutoff (pandas.DataFrame): + A dataframe that indicates cutoff time for each instance. + + Returns: + pandas.DataFrame, list: + * The generated feature matrix. + * List of feature definitions in the feature matrix. + """ + + fm_encoded, _ = self.featurization.generate_feature_matrix( + self.es, self.target_entity, cutoff) + fm_encoded = fm_encoded.reset_index(drop=True) + return fm_encoded + + def select_pipeline(self, pipeline): + """Select a pipeline. + + Args: + pipeline (MLPipeline or str): + A pipeline instance or the name/path of a pipeline. + """ + LOGGER.info("Selecting %s pipeline", pipeline) + self.modeler = Modeler(pipeline, self.chosen_problem.prediction_type) + + def train_test_split(self, X, y, test_size, shuffle): + """Split the training dataset and the testing dataset. + + Args: + X (pandas.DataFrame or ndarray): + Inputs to the pipeline. + y (pandas.Series or ndarray): + Target values. + test_size (float): + The proportion of the dataset to include in the test dataset. + shuffle (bool): + Whether or not to shuffle the data before splitting. + + Returns: + list: + List containing the train-test split of the inputs and targets. + """ + return self.modeler.train_test_split(X, y, test_size, shuffle) + + def fit(self, X, y, tune=False, max_evals=10, scoring=None, verbose=False): + """Train the cardea pipeline. + + Args: + X (pandas.DataFrame or ndarray): + Inputs to the pipeline. + y (pandas.Series ndarray): + Target values. + tune (bool): + Whether to optimize hyper-parameters of the pipelines. + max_evals (int): + Maximum number of hyper-parameter optimization iterations. + scoring (str): + The name of the scoring function used in the hyper-parameter optimization. + verbose (bool): + Whether to log information during processing. + """ + self.modeler.fit(X, y, tune, max_evals, scoring, verbose) + + def predict(self, X): + """Get predictions from the cardea pipeline. + + Args: + X (pandas.DataFrame or ndarray): + Inputs to the pipeline. + + Returns: + ndarray: + Predictions to the input data. + """ + return self.modeler.predict(X) + + def fit_predict(self, X, y, tune=False, max_evals=10, scoring=None, verbose=False): + """Train a cardea pipeline then make predictions. + + Args: + X (pandas.DataFrame or ndarray): + Inputs to the pipeline. + y (pandas.Series or ndarray): + Target values. + tune (bool): + Whether to optimize hyper-parameters of the pipelines. + max_evals (int): + Maximum number of hyper-parameter optimization iterations. + scoring (str): + The name of the scoring function used in the hyper-parameter optimization. + verbose (bool): + Whether to log information during processing. + + Returns: + ndarray: + Predictions to the input data. + """ + return self.modeler.fit_predict(X, y, tune, max_evals, scoring, verbose) + + def evaluate(self, X, y, test_size=0.2, shuffle=True, tune=False, max_evals=10, scoring=None, + metrics=None, verbose=False): + """Evaluate the cardea pipeline. + + Args: + X (pandas.DataFrame or ndarray): + Inputs to the pipeline. + y (pandas.Series or ndarray): + Target values. + test_size (float): + The proportion of the dataset to include in the test dataset. + shuffle (bool): + Whether or not to shuffle the data before splitting. + tune (bool): + Whether to optimize hyper-parameters of the pipelines. + max_evals (int): + Maximum number of hyper-parameter optimization iterations. + scoring (str): + The name of the scoring function used in the hyper-parameter optimization. + metrics (list): + A list of scoring function names. The scoring functions should be consistent + with the problem type. + verbose (bool): + Whether to log information during processing. + """ + return self.modeler.evaluate( + X, y, test_size, shuffle, tune, max_evals, scoring, metrics, verbose) + + def save(self, path): + """Save this object using pickle. + + Args: + path (str): + Path to the file where the serialization of + this object will be stored. + """ + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'wb') as pickle_file: + pickle.dump(self, pickle_file) + + @classmethod + def load(cls, path: str): + """Load an Orion instance from a pickle file. + + Args: + path (str): + Path to the file where the instance has been + previously serialized. + + Returns: + Cardea: + A Cardea instance + + Raises: + ValueError: + If the serialized object is not an Cardea instance. + """ + with open(path, 'rb') as pickle_file: + cardea = pickle.load(pickle_file) + if not isinstance(cardea, cls): + raise ValueError('Serialized object is not a Cardea instance') + + return cardea diff --git a/cardea/problem_definition/__init__.py b/cardea/problem_definition/__init__.py index 68e6f123..9e39a925 100644 --- a/cardea/problem_definition/__init__.py +++ b/cardea/problem_definition/__init__.py @@ -5,7 +5,7 @@ from cardea.problem_definition.predicting_diagnosis import DiagnosisPrediction from cardea.problem_definition.prolonged_length_of_stay import ProlongedLengthOfStay from cardea.problem_definition.readmission import Readmission -from cardea.problem_definition.show_noshow_appointment import MissedAppointmentProblemDefinition +from cardea.problem_definition.show_noshow_appointment import MissedAppointment __all__ = ( "ProblemDefinition", @@ -14,5 +14,5 @@ "DiagnosisPrediction", "ProlongedLengthOfStay", "Readmission", - "MissedAppointmentProblemDefinition" + "MissedAppointment" ) diff --git a/cardea/problem_definition/show_noshow_appointment.py b/cardea/problem_definition/show_noshow_appointment.py index 7e6feb3f..6093802f 100644 --- a/cardea/problem_definition/show_noshow_appointment.py +++ b/cardea/problem_definition/show_noshow_appointment.py @@ -3,7 +3,7 @@ from cardea.problem_definition import ProblemDefinition -class MissedAppointmentProblemDefinition (ProblemDefinition): +class MissedAppointment(ProblemDefinition): """Defines the problem of missed appointment Predict whether the patient will show to the appointment or not. diff --git a/docs/api_reference/cardea.rst b/docs/api_reference/cardea.rst index 9b5e6766..9f968a44 100644 --- a/docs/api_reference/cardea.rst +++ b/docs/api_reference/cardea.rst @@ -12,11 +12,16 @@ Cardea :toctree: api/ Cardea - Cardea.load_data_entityset + Cardea.load_entityset Cardea.list_problems Cardea.select_problem Cardea.list_feature_primitives Cardea.generate_features - Cardea.execute_model - Cardea.convert_to_json - Cardea.convert_from_json \ No newline at end of file + Cardea.select_pipeline + Cardea.train_test_split + Cardea.fit + Cardea.predict + Cardea.fit_predict + Cardea.evaluate + Cardea.save + Cardea.load \ No newline at end of file diff --git a/docs/api_reference/modeling.rst b/docs/api_reference/modeling.rst index 471917a8..350c5331 100644 --- a/docs/api_reference/modeling.rst +++ b/docs/api_reference/modeling.rst @@ -12,4 +12,10 @@ Modeler :toctree: api/ Modeler - Modeler.execute_pipeline \ No newline at end of file + Modeler.train_test_split + Modeler.fit + Modeler.predict + Modeler.fit_predict + Modeler.evaluate + Modeler.save + Modeler.load diff --git a/docs/api_reference/problem_definition.rst b/docs/api_reference/problem_definition.rst index 190dc94d..7a109238 100644 --- a/docs/api_reference/problem_definition.rst +++ b/docs/api_reference/problem_definition.rst @@ -56,5 +56,5 @@ MissedAppointmentProblemDefinition .. autosummary:: :toctree: api/ - MissedAppointmentProblemDefinition - MissedAppointmentProblemDefinition.generate_cutoff_times + MissedAppointment + MissedAppointment.generate_cutoff_times diff --git a/docs/authors.rst b/docs/authors.rst deleted file mode 100644 index e122f914..00000000 --- a/docs/authors.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../AUTHORS.rst diff --git a/docs/basic_concepts/index.rst b/docs/basic_concepts/index.rst index 6d54c8d2..cf0ebc8d 100644 --- a/docs/basic_concepts/index.rst +++ b/docs/basic_concepts/index.rst @@ -1,3 +1,5 @@ +.. _concepts: + Basic Concepts ============== diff --git a/docs/basic_concepts/machine_learning_tasks.rst b/docs/basic_concepts/machine_learning_tasks.rst index fbcb1fc6..f58a3992 100644 --- a/docs/basic_concepts/machine_learning_tasks.rst +++ b/docs/basic_concepts/machine_learning_tasks.rst @@ -35,8 +35,8 @@ values in the **Missed Appointment** task: from cardea import Cardea cardea = Cardea() - cardea.load_data_entityset() - cardea.select_problem('MissedAppointmentProblemDefinition') + cardea.load_entityset() + cardea.select_problem('MissedAppointment') Current Prediction Problems --------------------------- diff --git a/docs/community/contributing.rst b/docs/community/contributing.rst index 09af707e..f73491fc 100644 --- a/docs/community/contributing.rst +++ b/docs/community/contributing.rst @@ -6,13 +6,13 @@ Contributing Guidelines Ready to contribute with your own code? Great! Before diving deeper into the contributing guidelines, please make sure to having read -the :ref:`concepts` section and to have gone through the :ref:`development` guide. +the :ref:`concepts` section and to have gone through the development guide. Afterwards, please make sure to read the following contributing guidelines carefully, and later on head to the step-by-step guides for each possible type of contribution. General Coding Guidelines -************************* +------------------------- Once you have set up your development environment, you are ready to start working on your python code. @@ -76,7 +76,7 @@ When doing so, make sure to follow these guidelines: Unit Testing Guidelines -*********************** +----------------------- If you are going to contribute Python code, we will ask you to write unit tests that cover your development, following these requirements: diff --git a/docs/getting_started/quickstart.rst b/docs/getting_started/quickstart.rst index 861be6cb..8011a7d3 100644 --- a/docs/getting_started/quickstart.rst +++ b/docs/getting_started/quickstart.rst @@ -20,59 +20,60 @@ following command: .. ipython:: python - cardea.load_data_entityset() + cardea.load_entityset() + cardea.es You can see the list of problem definitions and select one with the following commands: .. ipython:: python cardea.list_problems() - problem = cardea.select_problem('MissedAppointmentProblemDefinition') -Then, you can perform the AutoML steps and take advantage of Cardea: +From there, you can select the prediction problem you aim to solve by specifying the name of the class, which in return gives us the ``label_times`` of the problem. -1. Extracting features (automated feature engineering), using the following commands: +.. ipython:: python - .. ipython:: python + label_times = cardea.select_problem('MissedAppointment') + label_times.head() - feature_matrix = cardea.generate_features(problem[:1000]) # a subset - feature_matrix = feature_matrix.sample(frac=1) # shuffle - y = list(feature_matrix.pop('label')) - X = feature_matrix.values +Then, you can perform the AutoML steps and take advantage of Cardea. -2. Modeling, optimizing hyperparameters and finding the most optimal model using the following commands: +Cardea extracts features through automated feature engineering by supplying the ``label_times`` pertaining to the problem you aim to solve, using the following commands: - .. ipython:: python + .. ipython:: python + :okwarning: - pipeline = [ - ['sklearn.ensemble.RandomForestClassifier'], - ['sklearn.naive_bayes.MultinomialNB'], - ['sklearn.neighbors.KNeighborsClassifier'] - ] + feature_matrix = cardea.generate_features(label_times[:1000]) # a subset + feature_matrix.head() - result = cardea.execute_model( - feature_matrix=X, - target=y, - primitives=pipeline - ) +Once we have the features, we can now split the data into training and testing + .. ipython:: python + :okwarning: -Finally, you can see accuracy results using the following commands: + y = list(feature_matrix.pop('label')) + X = feature_matrix.values + + X_train, X_test, y_train, y_test = cardea.train_test_split( + X, y, test_size=0.2, shuffle=True) -.. ipython:: python - import pandas as pd - from sklearn.metrics import accuracy_score +Now that we have our feature matrix properly divided, we can use to train our machine learning pipeline, Modeling, optimizing hyperparameters and finding the most optimal model is done using the following commands: - y_test = [] - y_pred = [] + .. ipython:: python + :okwarning: + + cardea.select_pipeline('Random Forest') + cardea.fit(X_train, y_train) + y_pred = cardea.predict(X_test) + + +Finally, you can see accuracy results using the following commands: - for i in range(0, 10): - y_test.extend(result['pipeline0']['folds'][str(i)]['Actual']) - y_pred.extend(result['pipeline0']['folds'][str(i)]['predicted']) + .. ipython:: python + :okwarning: + + cardea.evaluate(X, y, test_size=0.2, metrics=['Accuracy', 'F1 Macro']) - y_test = pd.Categorical(pd.Series(y_test)).codes - y_pred = pd.Categorical(pd.Series(y_pred)).codes - accuracy_score(y_test, y_pred) .. _Medical Appointment No Shows: https://www.kaggle.com/joniarroba/noshowappointments diff --git a/docs/images/cardea-process.png b/docs/images/cardea-process.png new file mode 100644 index 0000000000000000000000000000000000000000..21d8ab341eb0dd29ed09c97c0818df756f36b9fd GIT binary patch literal 71017 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�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 literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index 704f469a..98f68b93 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ An open source project from Data to AI Lab at MIT.

-|Development Status| |PyPi Shield| |Travis CI Shield| |Downloads| |Binder| +|Development Status| |PyPi Shield| |Run Tests| |Downloads| |Binder| Welcome to Cardea ================== @@ -14,7 +14,7 @@ Welcome to Cardea :width: 200 px :alt: Cardea Logo - An open source project from Data to AI Lab at MIT. +*This library is under development. Please contact dai-lab@mit.edu or any of the contributors for more information.* **Date**: |today| **Version**: |version| @@ -27,41 +27,30 @@ Welcome to Cardea Overview -------- -*This library is under development. Please contact dai-lab@mit.edu or any of the contributors -for more information.* +*This library is under development. Please contact dai-lab@mit.edu or any of the contributors for more information.* -Cardea is a machine learning library built on top of the `FHIR`_ data schema. The library uses -a number of AutoML tools developed under `The Human Data Interaction Project`_ at +Cardea is a machine learning library built on top of *schemas* that support electronic health records (EHR). The library uses a number of AutoML tools developed under `The Human Data Interaction Project`_ at `Data to AI Lab at MIT`_. -Our goal is to provide an easy to use library to develop machine learning models from -electronic health records. A typical usage of this library will involve: +Our goal is to provide an easy to use library to develop machine learning models from electronic health records. A typical usage of this library will involve interacting with our API to develop prediction models. -* Installing the library available via pypi +.. figure:: images/cardea-process.png + :width: 600 px + :alt: Cardea Process -* Integrating their data in FHIR schema (whatever subset of data is available) +A series of sequential processes are applied to build a machine learning model. These processes are triggered using our following APIs to perform the following: -* Following the API to develop some pre-specified prediction models (or specify new ones using - our API). The model building process is parameterized but automatically does: +* loading data using the automatic **data assembler**, where we capture data from its raw format into an entityset representation. - * data cleaning, auditing +* **data labeling** where we create label times that generates (1) the time index that indicates the timespan for which I create my features (2) the encoded labels of the prediction task. this is essential for our feature engineering phase. - * preprocessing +* **featurization** for which we automatically feature engineer our data to generate a feature matrix. - * feature engineering +* lastly, we build, train, and tune our machine learning model using the **modeling component**. - * machine learning model search and tuning - - * model evaluation - - * model auditing - -* Testing the models using our API - -* Preparing and deploying the models Explore Cardea ------------ +-------------- * `Getting Started `_ * `Basic Concepts `_ @@ -75,8 +64,8 @@ Explore Cardea :target: https://pypi.org/search/?c=Development+Status+%3A%3A+2+-+Pre-Alpha .. |PyPi Shield| image:: https://img.shields.io/pypi/v/cardea.svg :target: https://pypi.python.org/pypi/cardea -.. |Travis CI Shield| image:: https://travis-ci.org/MLBazaar/Cardea.svg?branch=master - :target: https://travis-ci.org/MLBazaar/Cardea +.. |Run Tests| image:: https://github.com/MLBazaar/Cardea/workflows/Run%20Tests/badge.svg + :target: https://github.com/MLBazaar/Cardea/actions?query=workflow%3A%22Run+Tests%22+branch%3Amaster .. |Downloads| image:: https://pepy.tech/badge/cardea :target: https://pepy.tech/project/cardea .. |Binder| image:: https://mybinder.org/badge_logo.svg diff --git a/setup.py b/setup.py index fe96f911..98a45108 100644 --- a/setup.py +++ b/setup.py @@ -3,10 +3,10 @@ from setuptools import setup, find_packages -with open('README.md') as readme_file: +with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() -with open('HISTORY.md') as history_file: +with open('HISTORY.md', encoding='utf-8') as history_file: history = history_file.read() install_requires = [ @@ -22,7 +22,8 @@ tests_require = [ 'pytest>=3.4.2', - 'google-compute-engine==2.8.12', # required by travis + 'pytest-cov>=2.6.0', + 'rundoc>=0.4.3,<0.5', ] development_requires = [ @@ -31,22 +32,21 @@ 'pip>=9.0.1', 'watchdog>=0.8.3', - # build docs + # docs 'm2r2>=0.2.5,<0.3', 'nbsphinx>=0.5.0,<0.7', - 'Sphinx>=1.7.1,<3', + 'Sphinx>=3,<3.3', 'pydata-sphinx-theme', 'autodocsumm>=0.1.10,<1', - 'recommonmark>=0.4.0', - 'ipython==6.5.0', + 'ipython>=6.5,<7.5', # style check - 'flake8>=3.5.0,<4', + 'flake8>=3.7.7,<4', 'isort>=4.3.4,<5', # automatically fix style issues - 'autoflake>=1.3', - 'autopep8>=1.3.5', + 'autoflake>=1.3,<2', + 'autopep8>=1.3.5,<2', # distribute on PyPI 'twine>=1.10.0', diff --git a/tests/cardea/problem_definition/test_show_noshow_appointment.py b/tests/cardea/problem_definition/test_show_noshow_appointment.py index 52032065..1a39df32 100644 --- a/tests/cardea/problem_definition/test_show_noshow_appointment.py +++ b/tests/cardea/problem_definition/test_show_noshow_appointment.py @@ -7,12 +7,12 @@ from numpy import nan from cardea.data_loader import EntitySetLoader -from cardea.problem_definition import MissedAppointmentProblemDefinition +from cardea.problem_definition import MissedAppointment @pytest.fixture() -def missed_appointment_problem_definition(): - return MissedAppointmentProblemDefinition() +def missed_appointment(): + return MissedAppointment() @pytest.fixture() @@ -126,31 +126,31 @@ def entityset_error_missing_cutoff_label(objects, objects_error_missing_cutoff_l def test_generate_cutoff_times_success( - es_success, missed_appointment_problem_definition, cutoff_times): - _, _, generated_df = missed_appointment_problem_definition.generate_cutoff_times(es_success) + es_success, missed_appointment, cutoff_times): + _, _, generated_df = missed_appointment.generate_cutoff_times(es_success) generated_df.index = cutoff_times.index # both should have the same index generated_df = generated_df[cutoff_times.columns] # same columns order assert generated_df.equals(cutoff_times) def test_generate_cutoff_times_error( - entityset_error_missing_label, missed_appointment_problem_definition): + entityset_error_missing_label, missed_appointment): with pytest.raises(ValueError): - missed_appointment_problem_definition.generate_cutoff_times( + missed_appointment.generate_cutoff_times( entityset_error_missing_label) -def test_generate_cutoff_times_error_value(es_success, missed_appointment_problem_definition): +def test_generate_cutoff_times_error_value(es_success, missed_appointment): es_success['Appointment'].df.loc[len(es_success['Appointment'].df)] = [ nan, nan, nan, nan, nan] with pytest.raises(ValueError): - missed_appointment_problem_definition.generate_cutoff_times( + missed_appointment.generate_cutoff_times( es_success) def test_generate_cutoff_times_missing_cutoff_time( - es_success, missed_appointment_problem_definition): + es_success, missed_appointment): es_success['Appointment'].delete_variables(['created']) with pytest.raises(ValueError): - missed_appointment_problem_definition.generate_cutoff_times( + missed_appointment.generate_cutoff_times( es_success) diff --git a/tox.ini b/tox.ini index 2e7dc506..6895ed00 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] -envlist = py35, py36, lint, docs +envlist = py37, py36, lint, docs [travis] python = 3.6: py36, lint, docs - 3.5: py35 + 3.7: py37 [testenv]