From e365085eb0ffc312900a3bc5b626353b5c6ca525 Mon Sep 17 00:00:00 2001 From: Graham Findlay Date: Fri, 28 May 2021 20:04:35 -0500 Subject: [PATCH] Initial commit --- .gitignore | 6 ++ README.md | 41 ++++++++ requirements.txt | 11 +++ setup.py | 40 ++++++++ shablona/__init__.py | 1 + shablona/shablona.py | 170 ++++++++++++++++++++++++++++++++ shablona/tests/__init__.py | 0 shablona/tests/test_shablona.py | 13 +++ shablona/version.py | 51 ++++++++++ 9 files changed, 333 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 shablona/__init__.py create mode 100644 shablona/shablona.py create mode 100644 shablona/tests/__init__.py create mode 100644 shablona/tests/test_shablona.py create mode 100644 shablona/version.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcaa640 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.py[co] +*~ +*.DS_Store +_build/ +auto_examples/ +gen_api/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca790ff --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +## shablona +A bare-bones template for Python packages, ready for use with setuptools (PyPI), pip, and py.test. + +### Using this as a template +Let's assume that you want to create a small scientific Python project called `smallish`. + +To use this repository as a template, click the green "use this template" button on the front page of the "shablona" repository. + +In "Repository name" enter the name of your project. For example, enter `smallish` here. After that, you can hit the "Create repository from template" button. + +You should then be able to clone the new repo into your machine. You will want to change the names of the files. For example, you will want to move `shablona/shablona.py` to be called `smallish/smallish.py` +``` +git mv shablona smallish +git mv smallish/shablona.py smallish/smallish.py +git mv smallish/tests/test_shablona.py smallish/tests/test_smallish.py +``` + +Make a commit recording these changes. Something like: +``` +git commit -a -m "Moved names from `shablona` to `smallish`" +``` + +You will want to edit a few more places that still have `shablona` in them. Type the following to see where all these files are: +``` +git grep shablona +``` + +You can replace `shablona` for `smallish` quickly with: +``` +git grep -l 'shablona' | xargs sed -i 's/shablona/smallish/g' +``` + +Edit `shablona/__init__.py`, and `shablona/version.py` with the information specific to your project. + +This very file (`README.md`) should be edited to reflect what your project is about. + +At this point, make another commit, and continue to develop your own code based on this template. + + +### Contributing +If you wish to make any changes (e.g. add documentation, tests, continuous integration, etc.), please follow the [Shablona](https://github.com/uwescience/shablona) template. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..687251a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# Per https://stackoverflow.com/questions/43658870/requirements-txt-vs-setup-py + +# requirements.txt +# +# installs dependencies from ./setup.py, and the package itself, +# in editable mode +-e . + +# (the -e above is optional). you could also just install the package +# normally with just the line below (after uncommenting) +# . \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dd4f047 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +import sys +import os +from setuptools import setup, find_packages +PACKAGES = find_packages() + +# Get version and release info, which is all stored in shablona/version.py +ver_file = os.path.join('shablona', 'version.py') +with open(ver_file) as f: + exec(f.read()) + +# Give setuptools a hint to complain if it's too old a version +# 24.2.0 added the python_requires option +# Should match pyproject.toml +SETUP_REQUIRES = ['setuptools >= 24.2.0'] +# This enables setuptools to install wheel on-the-fly +SETUP_REQUIRES += ['wheel'] if 'bdist_wheel' in sys.argv else [] + +opts = dict(name=NAME, + maintainer=MAINTAINER, + maintainer_email=MAINTAINER_EMAIL, + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + url=URL, + download_url=DOWNLOAD_URL, + license=LICENSE, + classifiers=CLASSIFIERS, + author=AUTHOR, + author_email=AUTHOR_EMAIL, + platforms=PLATFORMS, + version=VERSION, + packages=PACKAGES, + package_data=PACKAGE_DATA, + install_requires=REQUIRES, + python_requires=PYTHON_REQUIRES, + setup_requires=SETUP_REQUIRES, + requires=REQUIRES) + + +if __name__ == '__main__': + setup(**opts) diff --git a/shablona/__init__.py b/shablona/__init__.py new file mode 100644 index 0000000..782964d --- /dev/null +++ b/shablona/__init__.py @@ -0,0 +1 @@ +from .shablona import * # noqa diff --git a/shablona/shablona.py b/shablona/shablona.py new file mode 100644 index 0000000..3419ecc --- /dev/null +++ b/shablona/shablona.py @@ -0,0 +1,170 @@ + +from __future__ import absolute_import, division, print_function +import numpy as np +import pandas as pd +import scipy.optimize as opt +from scipy.special import erf + + +def transform_data(data): + """ + Function that takes experimental data and gives us the + dependent/independent variables for analysis. + Parameters + ---------- + data : Pandas DataFrame or string. + If this is a DataFrame, it should have the columns `contrast1` and + `answer` from which the dependent and independent variables will be + extracted. If this is a string, it should be the full path to a csv + file that contains data that can be read into a DataFrame with this + specification. + Returns + ------- + x : array + The unique contrast differences. + y : array + The proportion of '2' answers in each contrast difference + n : array + The number of trials in each x,y condition + """ + if isinstance(data, str): + data = pd.read_csv(data) + + contrast1 = data['contrast1'] + answers = data['answer'] + + x = np.unique(contrast1) + y = [] + n = [] + + for c in x: + idx = np.where(contrast1 == c) + n.append(float(len(idx[0]))) + answer1 = len(np.where(answers[idx[0]] == 1)[0]) + y.append(answer1 / n[-1]) + return x, y, n + + +def cumgauss(x, mu, sigma): + """ + The cumulative Gaussian at x, for the distribution with mean mu and + standard deviation sigma. + Parameters + ---------- + x : float or array + The values of x over which to evaluate the cumulative Gaussian function + mu : float + The mean parameter. Determines the x value at which the y value is 0.5 + sigma : float + The variance parameter. Determines the slope of the curve at the point + of Deflection + Returns + ------- + g : float or array + The cumulative gaussian with mean $\\mu$ and variance $\\sigma$ + evaluated at all points in `x`. + Notes + ----- + Based on: + http://en.wikipedia.org/wiki/Normal_distribution#Cumulative_distribution_function + The cumulative Gaussian function is defined as: + .. math:: + \\Phi(x) = \\frac{1}{2} [1 + erf(\\frac{x}{\\sqrt{2}})] + Where, $erf$, the error function is defined as: + .. math:: + erf(x) = \\frac{1}{\\sqrt{\\pi}} \\int_{-x}^{x} e^{t^2} dt + """ + return 0.5 * (1 + erf((x - mu) / (np.sqrt(2) * sigma))) + + +def opt_err_func(params, x, y, func): + """ + Error function for fitting a function using non-linear optimization. + Parameters + ---------- + params : tuple + A tuple with the parameters of `func` according to their order of + input + x : float array + An independent variable. + y : float array + The dependent variable. + func : function + A function with inputs: `(x, *params)` + Returns + ------- + float array + The marginals of the fit to x/y given the params + """ + return y - func(x, *params) + + +class Model(object): + """Class for fitting cumulative Gaussian functions to data""" + def __init__(self, func=cumgauss): + """ Initialize a model object. + Parameters + ---------- + data : Pandas DataFrame + Data from a subjective contrast judgement experiment + func : callable, optional + A function that relates x and y through a set of parameters. + Default: :func:`cumgauss` + """ + self.func = func + + def fit(self, x, y, initial=[0.5, 1]): + """ + Fit a Model to data. + Parameters + ---------- + x : float or array + The independent variable: contrast values presented in the + experiment + y : float or array + The dependent variable + Returns + ------- + fit : :class:`Fit` instance + A :class:`Fit` object that contains the parameters of the model. + """ + params, _ = opt.leastsq(opt_err_func, initial, + args=(x, y, self.func)) + return Fit(self, params) + + +class Fit(object): + """ + Class for representing a fit of a model to data + """ + def __init__(self, model, params): + """ + Initialize a :class:`Fit` object. + Parameters + ---------- + model : a :class:`Model` instance + An object representing the model used + params : array or list + The parameters of the model evaluated for the data + """ + self.model = model + self.params = params + + def predict(self, x): + """ + Predict values of the dependent variable based on values of the + indpendent variable. + Parameters + ---------- + x : float or array + Values of the independent variable. Can be values presented in + the experiment. For out-of-sample prediction (e.g. in + cross-validation), these can be values + that were not presented in the experiment. + Returns + ------- + y : float or array + Predicted values of the dependent variable, corresponding to + values of the independent variable. + """ + return self.model.func(x, *self.params) \ No newline at end of file diff --git a/shablona/tests/__init__.py b/shablona/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shablona/tests/test_shablona.py b/shablona/tests/test_shablona.py new file mode 100644 index 0000000..107002f --- /dev/null +++ b/shablona/tests/test_shablona.py @@ -0,0 +1,13 @@ +import os.path as op +import numpy.testing as npt +import shablona as ece + +data_path = op.join(ece.__path__[0], 'data') +#Load data like: op.join(data_path, 'mydatafile.dat') + + +def test_trivial(): + """ + Should always pass. Just used to ensure that py.test is setup correctly. + """ + npt.assert_equal(np.array([1, 1, 1]), np.array([1, 1, 1])) \ No newline at end of file diff --git a/shablona/version.py b/shablona/version.py new file mode 100644 index 0000000..6249f5d --- /dev/null +++ b/shablona/version.py @@ -0,0 +1,51 @@ +from os.path import join as pjoin + +# Format expected by setup.py and doc/source/conf.py: string of form "X.Y.Z" +_version_major = 0 +_version_minor = 1 +_version_micro = '' # use '' for first of series, number for 1 and above +_version_extra = 'dev' +# _version_extra = '' # Uncomment this for full releases + +# Construct full version string from these. +_ver = [_version_major, _version_minor] +if _version_micro: + _ver.append(_version_micro) +if _version_extra: + _ver.append(_version_extra) + +__version__ = '.'.join(map(str, _ver)) + +CLASSIFIERS = ["Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Scientific/Engineering"] + +# Description should be a one-liner: +description = "shablona: Put a one-liner description of your code here" +# Long description will go up on the pypi page +long_description = """ +shablona: Put a longer description of your code here. +""" + +NAME = "shablona" +MAINTAINER = "Graham Findlay" +MAINTAINER_EMAIL = "gfindlay@wisc.edu" +DESCRIPTION = description +LONG_DESCRIPTION = long_description +URL = "http://github.com/CSC-UW/shablona" +DOWNLOAD_URL = "" +LICENSE = "MIT" +AUTHOR = "Graham Findlay" +AUTHOR_EMAIL = "gfindlay@wisc.edu" +PLATFORMS = "OS Independent" +MAJOR = _version_major +MINOR = _version_minor +MICRO = _version_micro +VERSION = __version__ +PACKAGE_DATA = {'shablona': [pjoin('data', '*')]} +REQUIRES = ["numpy"] +PYTHON_REQUIRES = ">= 3.7" \ No newline at end of file