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 @@
-
+
+
+
+
+
+
pycatch22: CAnonical Time-series CHaracteristics in python
+
+
@@ -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