diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..409f6ce --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge 🤝 + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards 🌟 + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities 🛡️ + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope 🌐 + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement 👮🏻 + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement [here](joshua.moore@sydney.edu.au). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines 📋 + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction ✔️ + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning ❗ + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban ⏳ + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban ❌ + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution 👍 + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..e640314 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,17 @@ +# Reporting Security Issues 🛡️ + +The _catch22_ team and community take security bugs in _catch22_ seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. + +To report a security issue, please use the GitHub Security Advisory tab. + +The _catch22_ team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. + +## Supported Versions + +The following versions of _catch22_ are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 0.5.0 | :white_check_mark: | +| 0.4.5 | :white_check_mark: | diff --git a/.github/workflows/run_unit_tests.yaml b/.github/workflows/run_unit_tests.yaml index 637ab41..80a2086 100644 --- a/.github/workflows/run_unit_tests.yaml +++ b/.github/workflows/run_unit_tests.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Setup python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 077131d..d913ebb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ -

catch22 logo

+

+ + + catch22 logo + +

pycatch22: CAnonical Time-series CHaracteristics in python

+
+ Python 3.8 | 3.9 | 3.10 | 3.11 | 3.12

@@ -63,7 +70,10 @@ tsData = [1,2,4,3] # (or more interesting data!) pycatch22.CO_f1ecac(tsData) ``` -All features are bundled in the method `catch22_all`, which also accepts `numpy` arrays and gives back a dictionary containing the entries `catch22_all['names']` for feature names and `catch22_all['values']` for feature outputs. +All features are bundled in the method `catch22_all`, which also accepts `numpy` arrays and gives back a `DataFrame` containing the columns: +- `feature` for (short) feature names (as outlined in the GitBook [Feature overview table](https://time-series-features.gitbook.io/catch22/feature-descriptions/feature-overview-table)). +- `hctsa_name` for (long) feature names as they appear in HCTSA. +- `value` for feature outputs. Usage (computing 22 features: _catch22_): @@ -77,13 +87,6 @@ Usage (computing 24 features: _catch24_ = _catch22_ + mean + standard deviation) pycatch22.catch22_all(tsData,catch24=True) ``` -We also include a 'short name' for each feature for easier reference (as outlined in the GitBook [Feature overview table](https://time-series-features.gitbook.io/catch22/feature-descriptions/feature-overview-table)). -These short names can be included in the output from `catch22_all()` by setting `short_names=True` as follows: - -```python3 -pycatch22.catch22_all(tsData,catch24=True,short_names=True) -``` - ### Template analysis script Thanks to [@jmoo2880](https://github.com/jmoo2880) for putting together a [demonstration notebook](https://github.com/jmoo2880/c22-usage-examples/) for using pycatch22 to extract features from a time-series dataset. diff --git a/img/catch22_logo_square_darkmode.png b/img/catch22_logo_square_darkmode.png new file mode 100644 index 0000000..184ca0a Binary files /dev/null and b/img/catch22_logo_square_darkmode.png differ diff --git a/pyproject.toml b/pyproject.toml index de9cddb..9d24a38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pycatch22" -version = "0.4.5" +version = "1.0.0" authors = [ {name = "Carl H Lubba"}, {email = "carl.lubba@gmx.de"}, @@ -17,6 +17,10 @@ classifiers = [ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", ] +dependencies = [ + "pandas", + "numpy" +] [project.urls] "GitHub Repository (pycatch22)" = "https://github.com/DynamicsAndNeuralSystems/pycatch22" diff --git a/setup.py b/setup.py index fc2a169..387e4ce 100644 --- a/setup.py +++ b/setup.py @@ -26,5 +26,6 @@ packages = find_packages(where = "src", include = ["pycatch22"]), package_dir = {"": "src"}, - ext_modules = [extension_mod] + ext_modules = [extension_mod], + install_requires = ["numpy", "pandas"] ) diff --git a/src/pycatch22/catch22.py b/src/pycatch22/catch22.py index 5eaa932..e9338d8 100644 --- a/src/pycatch22/catch22.py +++ b/src/pycatch22/catch22.py @@ -1,6 +1,7 @@ import catch22_C +import pandas as pd -def catch22_all(data, catch24=False, short_names=False): +def catch22_all(data, catch24=False): ''' Extract the catch22 feature set from an input time series. @@ -10,12 +11,10 @@ def catch22_all(data, catch24=False, short_names=False): Input time-series data. catch24 : bool, optional If True, include the two catch24 features (mean and standard deviation) in the output. - short_names : bool, optional - If True, also include the short names of the features in the output. ''' - features = [ + features_hctsa = [ 'DN_HistogramMode_5', 'DN_HistogramMode_10', 'CO_f1ecac', @@ -40,7 +39,7 @@ def catch22_all(data, catch24=False, short_names=False): 'FC_LocalSimple_mean3_stderr' ] - features_short = [ + features = [ 'mode_5', 'mode_10', 'acf_timescale', @@ -66,18 +65,18 @@ def catch22_all(data, catch24=False, short_names=False): ] if catch24: - features.append('DN_Mean') - features.append('DN_Spread_Std') - features_short.append('mean') - features_short.append('SD') + features_hctsa.append('DN_Mean') + features_hctsa.append('DN_Spread_Std') + features.append('mean') + features.append('SD') data = list(data) featureOut = [] - for f in features: + for f in features_hctsa: featureFun = getattr(catch22_C, f) featureOut.append(featureFun(data)) - if short_names: - return {'names': features, 'short_names': features_short, 'values': featureOut} - else: - return {'names': features, 'values': featureOut} + # convert to a dataframe + feature_results = pd.DataFrame({'feature': features, 'value': featureOut, 'hctsa_name': features_hctsa}) + + return feature_results diff --git a/tests/test_features.py b/tests/test_features.py index 3b20542..5dc50b3 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -50,7 +50,7 @@ def load_expected_outputs(): def compute_new_features(): """Computes new feature outputs on same benchmarking dataset and then returns dictionary of datasets in the same format as - the loaded expected outputs dictionary""" + the loaded expected outputs dictionary for direct comparison.""" benchmark_datasets = load_benchmark_datasets() datasets = benchmark_datasets.keys() @@ -60,7 +60,7 @@ def compute_new_features(): print(f"Computing features for: {dset}...") test_data = benchmark_datasets[dset] res = catch22.catch22_all(test_data, catch24=True) - for (name, val) in zip(res['names'], res['values']): + for (name, val) in zip(res['hctsa_name'], res['value']): dataset_dict_single[name] = float(val) dataset_dicts[dset] = dataset_dict_single diff --git a/tests/unit_tests.py b/tests/unit_tests.py index 48e2098..5082fa8 100644 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -1,60 +1,51 @@ import pycatch22 as catch22 import pytest import numpy as np +import pandas as pd # unit tests -def expected_output(res, catch24=False, short_names=False): +def expected_output(res, catch24=False): which_set = "Catch24" if catch24 else "Catch22" num_features = 24 if catch24 else 22 # check if the output is a dictionary - assert isinstance(res, dict), f"{which_set} did not return a dictionary. Unexpected output." - # check if the dictionary has two keys - names and values - if short_names: - dict_length = len(res) - assert dict_length == 3, f"{which_set} returned a dictionary of length {dict_length}. Expected length 3 for short_names = true" - assert all(key in ['names', 'short_names', 'values'] for key in res.keys()), f"{which_set} returned unexpected keys for short_names = True" - - # check the short names list - assert all(isinstance(name, str) for name in res['short_names']), f"{which_set} expected all returned short names to be strings." - length_of_names = len(res['short_names']) - assert length_of_names == num_features, f"Expected {num_features} short names for {which_set}, got {length_of_names} instead." - - else: - assert len(res) == 2, f"{which_set} returned an unexpected dictionary size." - # check if the keys are 'names' and 'values' - assert all(key in ['names', 'values'] for key in res.keys()), f"{which_set} returned unexpected keys." - - # check the 'names' list - assert isinstance(res['names'], list), f"{which_set} expected list of names (str), got unexpected output." - length_of_names = len(res['names']) + assert isinstance(res, pd.DataFrame), f"{which_set} did not return a DataFrame. Unexpected output." + # check if the dataframe has three columns - features, values and hctsa_names + assert len(res.columns) == 3, f"{which_set} returned a DataFrame with an unexpected number of columns." + assert all(col_name in ['feature', 'value', 'hctsa_name'] for col_name in res.columns), f"{which_set} returned unexpected columns." + # check the column datatypes + assert all(isinstance(fname, str) for fname in res.feature), f"{which_set} expected all returned feature names to be strings." + length_of_names = len(res.feature) assert length_of_names == num_features, f"Expected {num_features} names for {which_set}, got {length_of_names} instead." - assert all(isinstance(name, str) for name in res['names']), f"{which_set} expected all returned names to be strings." - # check the 'values' list - assert isinstance(res['values'], list), f"{which_set} expected list of values, got unexpected output." - length_of_vals = len(res['values']) + # check the 'value' column + length_of_vals = len(res.value) assert length_of_vals == num_features, f"Expected {num_features} values for {which_set}, got {length_of_vals} instead." - assert all(isinstance(val, (float, int)) for val in res['values']), f"{which_set} expected all returned feature values to be floats or integers." + assert all(isinstance(val, (float, int)) for val in res.value), f"{which_set} expected all returned feature values to be floats or integers." + + # check the `hctsa_name` column + length_of_hctsa_names = len(res.hctsa_name) + assert length_of_hctsa_names == num_features, f"Expected {num_features} hctsa names for {which_set}, got {length_of_hctsa_names} instead." + assert all(isinstance(fname, str) for fname in res.hctsa_name), f"{which_set} expected all returned hctsa feature names to be strings." def test_catch22_runs(): # test whether catch22 works on some random data tsData = np.random.randn(100) - res = catch22.catch22_all(tsData, catch24=False, short_names=False) - expected_output(res, catch24=False, short_names=False) + res = catch22.catch22_all(tsData, catch24=False) + expected_output(res, catch24=False) def test_catch24_runs(): # test whether catch24 works on some random data tsData = np.random.randn(100) - res = catch22.catch22_all(tsData, catch24=True, short_names=False) - expected_output(res, catch24=True, short_names=False) + res = catch22.catch22_all(tsData, catch24=True) + expected_output(res, catch24=True) def test_short_names_returned(): # test whether catch22/catch24 returns short names tsData = np.random.randn(100) - res = catch22.catch22_all(tsData, catch24=False, short_names=True) - expected_output(res, catch24=False, short_names=True) - res2 = catch22.catch22_all(tsData, catch24=True, short_names=True) - expected_output(res2, catch24=True, short_names=True) + res = catch22.catch22_all(tsData, catch24=False) + expected_output(res, catch24=False) + res2 = catch22.catch22_all(tsData, catch24=True) + expected_output(res2, catch24=True) def test_valid_input_types(): # should accept tuples, arrays and lists @@ -76,8 +67,8 @@ def test_inf_and_nan_input(): base_data = np.random.randn(100) base_data[0] = val_type res = catch22.catch22_all(base_data, catch24=False) - expected_output(res, catch24=False, short_names=False) - res_values = res['values'] + expected_output(res, catch24=False) + res_values = res['value'] for i, val in enumerate(res_values): if i in zero_outputs: # check that value is 0 or 0.0