diff --git a/setup.cfg b/setup.cfg index e689ed9..3ae44e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,8 @@ ignore = F401, # Imported but unused B028, # No explicit stacklevel keyword argument found W293, # blank line contains whitespace (I don't like this rule, interferes with good function indentation) C419, # Unnecessary list comprehension passed to all() prevents short-circuiting - rewrite as a generator - + E702, # Multiple statements on one line (semicolon) + per-file-ignores = # It poses no inconvenient to violate B006 in this file. tests/test_ageml/test_modelling.py: B006 diff --git a/src/ageml/__main__.py b/src/ageml/__main__.py index 990ad59..0b69cd1 100644 --- a/src/ageml/__main__.py +++ b/src/ageml/__main__.py @@ -1,9 +1,10 @@ import ageml + def main(): """Run age modelling interactive command line interface.""" - ageml.ui.InteractiveCLI() + ageml.ui.CLI() if __name__ == '__main__': diff --git a/src/ageml/commands.py b/src/ageml/commands.py index ac51cd9..0431254 100644 --- a/src/ageml/commands.py +++ b/src/ageml/commands.py @@ -1,3 +1,23 @@ +"""Command line commands. + +Used in the AgeML project with poetry to create command line commands. + +Public classes: +--------------- +ModelAge: Run age modelling with required parameters. +FactorAnalysis: Run factor analysis with required parameters. +ClinicalGroups: Run clinical analysis with age deltas with required parameters. +ClinicalClassification: Run classification of groups based on age deltas with required parameters. + + +Public functions: +----------------- +model_age(): Run ModelAge class. +factor_analysis(): Run FactorAnalysis class. +clinical_groups(): Run ClinicalGroups class. +clinical_classify(): Run ClinicalClassification class. +""" + import argparse import ageml.messages as messages @@ -5,15 +25,9 @@ from ageml.ui import Interface from ageml.utils import convert -class ModelAge(Interface): - """Read and parses user commands via command line. - - Public methods: - --------------- - configure_parser(self): Configure parser with required arguments for processing. - configure_args(self, args): Configure arguments with required fromatting for modelling. - """ +class ModelAge(Interface): + """Run age modelling with required parameters.""" def __init__(self): """Initialise variables.""" @@ -40,27 +54,27 @@ def configure_parser(self): # Required arguments self.parser.add_argument("-o", "--output", metavar="DIR", required=True, - help=messages.output_long_description,) + help=messages.output_long_description,) self.parser.add_argument("-f", "--features", metavar="FILE", required=True, - help=messages.features_long_description) + help=messages.features_long_description) # Parameter arguments with defaults - self.parser.add_argument("-m","--model", nargs="*", default=["linear"], - help=messages.model_long_description) + self.parser.add_argument("-m", "--model", nargs="*", default=["linear"], + help=messages.model_long_description) self.parser.add_argument("-s", "--scaler", nargs="*", default=["standard"], - help=messages.scaler_long_description) + help=messages.scaler_long_description) self.parser.add_argument("--cv", nargs="+", type=int, default=[5, 0], - help=messages.cv_long_description) + help=messages.cv_long_description) # Optional arguments self.parser.add_argument("--covariates", metavar="FILE", - help=messages.covar_long_description) + help=messages.covar_long_description) self.parser.add_argument("--covar_name", metavar="COVAR_NAME", - help=messages.covar_name_long_description) + help=messages.covar_name_long_description) self.parser.add_argument("--clinical", metavar="FILE", - help=messages.clinical_long_description) + help=messages.clinical_long_description) self.parser.add_argument("--systems", metavar="FILE", - help=messages.systems_long_description) + help=messages.systems_long_description) def configure_args(self, args): """Configure argumens with required fromatting for modelling. @@ -117,13 +131,9 @@ def configure_args(self, args): return args -class FactorAnalsyis(Interface): - """Read and parses user commands via command line. - Public methods: - --------------- - configure_parser(self): Configure parser with required arguments for processing. - """ +class FactorAnalsyis(Interface): + """Run factor analysis with required parameters.""" def __init__(self): """Initialise variables.""" @@ -149,25 +159,21 @@ def configure_parser(self): # Required arguments self.parser.add_argument("-o", "--output", metavar="DIR", required=True, - help=messages.output_long_description,) + help=messages.output_long_description,) self.parser.add_argument("-a", "--ages", metavar="FILE", required=True, - help=messages.ages_long_description) + help=messages.ages_long_description) self.parser.add_argument("-f", "--factors", metavar="FILE", required=True, - help=messages.factors_long_description) + help=messages.factors_long_description) # Optional arguments self.parser.add_argument("--covariates", metavar="FILE", - help=messages.covar_long_description) + help=messages.covar_long_description) self.parser.add_argument("--clinical", metavar="FILE", - help=messages.clinical_long_description) + help=messages.clinical_long_description) -class ClinicalGroups(Interface): - """Read and parses user commands via command line. - Public methods: - --------------- - configure_parser(self): Configure parser with required arguments for processing. - """ +class ClinicalGroups(Interface): + """Run clinical analysis with age deltas with required parameters.""" def __init__(self): """Initialise variables.""" @@ -193,19 +199,15 @@ def configure_parser(self): # Required arguments self.parser.add_argument("-o", "--output", metavar="DIR", required=True, - help=messages.output_long_description,) + help=messages.output_long_description,) self.parser.add_argument("-a", "--ages", metavar="FILE", required=True, - help=messages.ages_long_description) + help=messages.ages_long_description) self.parser.add_argument("--clinical", metavar="FILE", required=True, - help=messages.clinical_long_description) + help=messages.clinical_long_description) -class ClinicalClassification(Interface): - """Read and parses user commands via command line. - Public methods: - --------------- - configure_parser(self): Configure parser with required arguments for processing. - """ +class ClinicalClassification(Interface): + """Run classification of groups based on age deltas with required parameters.""" def __init__(self): """Initialise variables.""" @@ -234,30 +236,36 @@ def configure_parser(self): # Required arguments self.parser.add_argument("-o", "--output", metavar="DIR", required=True, - help=messages.output_long_description,) + help=messages.output_long_description,) self.parser.add_argument("-a", "--ages", metavar="FILE", required=True, - help=messages.ages_long_description) + help=messages.ages_long_description) self.parser.add_argument("--clinical", metavar="FILE", required=True, - help=messages.clinical_long_description) + help=messages.clinical_long_description) self.parser.add_argument("--groups", nargs=2, metavar="GROUP", required=True, - help=messages.groups_long_description) + help=messages.groups_long_description) + + +# Object wrappers def model_age(): - """Run model_age class.""" + """Run ModelAge class.""" ModelAge() + def factor_analysis(): - """Run factor_analysis class.""" + """Run FactorAnalysis class.""" FactorAnalsyis() + def clinical_groups(): - """Run clinical_groups class.""" + """Run ClinicalGroups class.""" ClinicalGroups() + def clinical_classify(): - """Run clinical_classify class.""" + """Run ClinicalClassification class.""" ClinicalClassification() diff --git a/src/ageml/ui.py b/src/ageml/ui.py index 043bc61..b26aa45 100644 --- a/src/ageml/ui.py +++ b/src/ageml/ui.py @@ -71,13 +71,13 @@ class Interface: run_wrapper(self, run): Wrapper for running modelling with log. - run_age(self): Run basic age modelling. + run_age(self): Run age modelling. - run_factor_analysis(self): Run age modelling with lifestyle factors. + run_factor_analysis(self): Factor analysis between deltas and factors. - run_clinical(self): Run age modelling with clinical factors. + run_clinical(self): Analyse differences between deltas in clinical groups. - run_classification(self): Run classification between two different clinical groups. + run_classification(self): Classify groups based on deltas. """ def __init__(self, args): @@ -553,7 +553,7 @@ def run_wrapper(self, run): run() def run_age(self): - """Run basic age modelling.""" + """Run age modelling.""" # Run age modelling print("Running age modelling...") @@ -637,7 +637,7 @@ def run_age(self): self.df_ages.to_csv(os.path.join(self.dir_path, filename)) def run_factor_analysis(self): - """Run age modelling with lifestyle factors.""" + """Run factor analysis between deltas and factors.""" print("Running lifestyle factors...") @@ -667,7 +667,7 @@ def run_factor_analysis(self): self.factors_vs_deltas(dfs_ages, dfs_factors, groups, self.df_factors.columns.to_list()) def run_clinical(self): - """Run age modelling with clinical factors.""" + """Analyse differences between deltas in clinical groups.""" print("Running clinical outcomes...") @@ -727,7 +727,8 @@ def run_classification(self): self.set_classifier() self.classify(df_group1, df_group2, groups) -class InteractiveCLI(Interface): + +class CLI(Interface): """Read and parses user commands via command line via an interactive interface diff --git a/tests/test_ageml/test_commands.py b/tests/test_ageml/test_commands.py new file mode 100644 index 0000000..3c76f93 --- /dev/null +++ b/tests/test_ageml/test_commands.py @@ -0,0 +1,165 @@ +import os +import pandas as pd +import pytest +import random +import string +import sys +import tempfile + +from ageml.commands import model_age, factor_analysis, clinical_groups, clinical_classify + + +# Fake data for testing +@pytest.fixture +def features(): + df = pd.DataFrame( + { + "id": [1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "age": [50, 55, 60, 65, 70, 75, 80, 85, 90, 57, + 53, 57, 61, 65, 69, 73, 77, 81, 85, 89], + "feature1": [1.3, 2.2, 3.9, 4.1, 5.7, 6.4, 7.5, 8.2, 9.4, 1.7, + 1.4, 2.2, 3.8, 4.5, 5.4, 6.2, 7.8, 8.2, 9.2, 2.6], + "feature2": [9.4, 8.2, 7.5, 6.4, 5.3, 4.1, 3.9, 2.2, 1.3, 9.4, + 9.3, 8.1, 7.9, 6.5, 5.0, 4.0, 3.7, 2.1, 1.4, 8.3], + } + ) + df.set_index("id", inplace=True) + return df + + +@pytest.fixture +def factors(): + df = pd.DataFrame( + { + "id": [1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "factor1": [1.3, 2.2, 3.9, 4.1, 5.7, 6.4, 7.5, 8.2, 9.4, 1.3, + 1.3, 2.2, 3.9, 4.1, 5.7, 6.4, 7.5, 8.2, 9.4, 2.2], + "factor2": [0.1, 1.3, 2.2, 3.9, 4.1, 5.7, 6.4, 7.5, 8.2, 9.4, + 4.7, 3.7, 2.3, 1.2, 0.9, 0.3, 0.2, 0.1, 0.1, 0.1], + } + ) + df.set_index("id", inplace=True) + return df + + +@pytest.fixture +def clinical(): + df = pd.DataFrame( + { + "id": [1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "CN": [True, False, True, False, True, False, True, False, True, False, + True, False, True, False, True, False, True, False, True, False], + "group1": [False, True, False, True, False, True, False, True, False, True, + False, True, False, True, False, True, False, True, False, True], + } + ) + df.set_index("id", inplace=True) + return df + + +@pytest.fixture +def ages(): + df = pd.DataFrame( + { + "id": [1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "age": [50, 55, 60, 65, 70, 75, 80, 85, 90, 57, + 53, 57, 61, 65, 69, 73, 77, 81, 85, 89], + "predicted age": [55, 67, 57, 75, 85, 64, 87, 93, 49, 51, + 58, 73, 80, 89, 55, 67, 57, 75, 85, 64], + "corrected age": [51, 58, 73, 80, 89, 67, 57, 75, 85, 64, + 87, 93, 49, 55, 67, 57, 75, 85, 64, 87], + "delta": [1, -2, 3, 0, -1, 2, 1, 0, -3, 1, + 2, 1, 0, -1, 2, 1, 0, -3, 1, 2], + } + ) + df.set_index("id", inplace=True) + return df + + +def create_csv(df, path): + # Generate random name for the csv file + letters = string.ascii_lowercase + csv_name = "".join(random.choice(letters) for i in range(20)) + ".csv" + file_path = os.path.join(path, csv_name) + df.to_csv(path_or_buf=file_path, index=True) + return file_path + + +# Create temporary directory +@pytest.fixture +def temp_dir(): + return tempfile.TemporaryDirectory() + + +def test_model_age(temp_dir, features): + """Test model_age function.""" + + # Create features file + features_data_path = create_csv(features, temp_dir.name) + + # Create systems arguments + sys.argv = ["", + "-o", temp_dir.name, + "-f", features_data_path, + "-m", "linear", "fit_intercept=True", + "-s", "standard", + "--cv", "5", "0"] + + # Run function + model_age() + + +def test_factor_analysis(temp_dir, ages, factors): + """Test factor_analysis function.""" + + # Create features file + age_data_path = create_csv(ages, temp_dir.name) + factors_data_path = create_csv(factors, temp_dir.name) + + # Create systems arguments + sys.argv = ["", + "-o", temp_dir.name, + "-a", age_data_path, + "-f", factors_data_path] + + # Run function + factor_analysis() + + +def test_clinical_groups(temp_dir, ages, clinical): + """Test clinical_groups function.""" + + # Create features file + age_data_path = create_csv(ages, temp_dir.name) + clinical_data_path = create_csv(clinical, temp_dir.name) + + # Create systems arguments + sys.argv = ["", + "-o", temp_dir.name, + "-a", age_data_path, + "--clinical", clinical_data_path] + + # Run function + clinical_groups() + + +def test_clinical_classify(temp_dir, ages, clinical): + """Test age_deltas function.""" + + # Create features file + age_data_path = create_csv(ages, temp_dir.name) + clinical_data_path = create_csv(clinical, temp_dir.name) + + # Create systems arguments + sys.argv = ["", + "-o", temp_dir.name, + "-a", age_data_path, + "--clinical", clinical_data_path, + "--groups", "CN", "group1"] + + # Run function + clinical_classify() diff --git a/tests/test_ageml/test_ui.py b/tests/test_ageml/test_ui.py index d85ce51..9cdde0d 100644 --- a/tests/test_ageml/test_ui.py +++ b/tests/test_ageml/test_ui.py @@ -1,7 +1,6 @@ import os import pytest import shutil -import sys import tempfile import random import string @@ -9,7 +8,7 @@ import numpy as np import ageml.messages as messages -from ageml.ui import Interface, CLI, InteractiveCLI +from ageml.ui import Interface, CLI class ExampleArguments(object): @@ -147,7 +146,7 @@ def dummy_cli(monkeypatch): # Patch the input function monkeypatch.setattr("builtins.input", lambda _: responses.pop(0)) - interface = InteractiveCLI() + interface = CLI() return interface @@ -167,7 +166,8 @@ def test_interface_setup(dummy_interface): def test_load_csv(dummy_interface, features): features_path = create_csv(features, dummy_interface.dir_path) - data = dummy_interface.load_csv(features_path) + dummy_interface.args.features = features_path + data = dummy_interface.load_csv('features') # Check that the data is a pandas dataframe assert isinstance(data, pd.core.frame.DataFrame) @@ -367,13 +367,13 @@ def test_run_age(dummy_interface, features): # TODO: def test_run_age_with_covars(dummy_interface, ages, features, covariates): -def test_run_lifestyle(dummy_interface, ages, factors): +def test_run_factor_analysis(dummy_interface, ages, factors): # Run the lifestyle pipeline ages_path = create_csv(ages, dummy_interface.dir_path) factors_path = create_csv(factors, dummy_interface.dir_path) dummy_interface.args.ages = ages_path dummy_interface.args.factors = factors_path - dummy_interface.run_lifestyle() + dummy_interface.run_factor_analysis() # Check for the existence of the output directory assert os.path.exists(dummy_interface.dir_path) @@ -479,35 +479,6 @@ def test_interface_setup_dir_existing_warning(dummy_interface): assert warn_record.list[0].message.args[0] == error_message -def test_cli_initialization(features, dummy_interface): - # create features file - features_data_path = create_csv(features, dummy_interface.dir_path) - - output_path = os.path.dirname(__file__) - # Path sys.argv (command line arguments) - # sys.argv[0] should be empty, so we set it to '' - # TODO: Cleaner way to test CLI? - sys.argv = [ - "", - "-f", - features_data_path, - "-o", - output_path, - "-r", - "age", - "--cv", - "2", - "1", - ] - cli = CLI() - - # Check correct default initialization - assert cli.args.features == features_data_path - assert cli.args.model == ["linear"] - assert cli.args.scaler_type == "standard" - assert cli.args.cv == [2, 1] - - def test_configure_interactiveCLI(dummy_cli): """Test dummy InteractiveCLI configured""" assert dummy_cli.configFlag is True