From 04d818c0f9b9a9a349b785c1ee6f273aeb288551 Mon Sep 17 00:00:00 2001 From: ByteYJ <145717481+ByteYJ@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:00:48 -0400 Subject: [PATCH] Initial commit --- .dvc/.gitignore | 3 + .dvc/config | 0 .dvcignore | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 35 +++ .github/ISSUE_TEMPLATE/user-story.md | 25 ++ .github/workflows/python_package.yml | 36 +++ .github/workflows/ruff.yml | 8 + .gitignore | 129 ++++++++ CHANGELOG.md | 33 ++ CONTRIBUTIONS.md | 38 +++ LICENSE.md | 21 ++ README.md | 231 ++++++++++++++ dag_workflow.png | Bin 0 -> 20788 bytes data/.gitignore | 1 + data/gutenberg/austen-emma.txt | 25 ++ data/gutenberg/austen-persuasion.txt | 24 ++ data/gutenberg/austen-sense.txt | 22 ++ data/gutenberg/bible-kjv.txt | 32 ++ data/gutenberg/blake-poems.txt | 47 +++ data/gutenberg/bryant-stories.txt | 40 +++ data/gutenberg/burgess-busterbrown.txt | 22 ++ data/gutenberg/carroll-alice.txt | 21 ++ data/gutenberg/chesterton-ball.txt | 21 ++ data/gutenberg/chesterton-brown.txt | 20 ++ data/gutenberg/chesterton-thursday.txt | 20 ++ docs/Makefile | 20 ++ docs/conf.py | 55 ++++ docs/index.rst | 24 ++ docs/make.bat | 35 +++ dvc.lock | 18 ++ dvc.yaml | 8 + notebooks/word_count_prototype.ipynb | 372 +++++++++++++++++++++++ pyproject.toml | 47 +++ src/cdstemplate/__init__.py | 0 src/cdstemplate/corpus_counter_script.py | 68 +++++ src/cdstemplate/utils.py | 12 + src/cdstemplate/word_count.py | 114 +++++++ tests/test_word_count.py | 105 +++++++ 38 files changed, 1735 insertions(+) create mode 100644 .dvc/.gitignore create mode 100644 .dvc/config create mode 100644 .dvcignore create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/user-story.md create mode 100644 .github/workflows/python_package.yml create mode 100644 .github/workflows/ruff.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTIONS.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 dag_workflow.png create mode 100644 data/.gitignore create mode 100644 data/gutenberg/austen-emma.txt create mode 100644 data/gutenberg/austen-persuasion.txt create mode 100644 data/gutenberg/austen-sense.txt create mode 100644 data/gutenberg/bible-kjv.txt create mode 100644 data/gutenberg/blake-poems.txt create mode 100644 data/gutenberg/bryant-stories.txt create mode 100644 data/gutenberg/burgess-busterbrown.txt create mode 100644 data/gutenberg/carroll-alice.txt create mode 100644 data/gutenberg/chesterton-ball.txt create mode 100644 data/gutenberg/chesterton-brown.txt create mode 100644 data/gutenberg/chesterton-thursday.txt create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 dvc.lock create mode 100644 dvc.yaml create mode 100644 notebooks/word_count_prototype.ipynb create mode 100644 pyproject.toml create mode 100644 src/cdstemplate/__init__.py create mode 100644 src/cdstemplate/corpus_counter_script.py create mode 100644 src/cdstemplate/utils.py create mode 100644 src/cdstemplate/word_count.py create mode 100644 tests/test_word_count.py diff --git a/.dvc/.gitignore b/.dvc/.gitignore new file mode 100644 index 0000000..528f30c --- /dev/null +++ b/.dvc/.gitignore @@ -0,0 +1,3 @@ +/config.local +/tmp +/cache diff --git a/.dvc/config b/.dvc/config new file mode 100644 index 0000000..e69de29 diff --git a/.dvcignore b/.dvcignore new file mode 100644 index 0000000..5197305 --- /dev/null +++ b/.dvcignore @@ -0,0 +1,3 @@ +# Add patterns of files dvc should ignore, which could improve +# the performance. Learn more at +# https://dvc.org/doc/user-guide/dvcignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..aa9237f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Data Resources** +Please provide any data resources, such as model files, input data or configuration files associated with this issue. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Python version [e.g. 3.8] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/user-story.md b/.github/ISSUE_TEMPLATE/user-story.md new file mode 100644 index 0000000..75c9f44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/user-story.md @@ -0,0 +1,25 @@ +--- +name: User Story +about: Describe context and goals for providing value to users in this project. +title: '' +labels: '' +assignees: '' + +--- + +As a [user concerned] +I want [goal] +so that [reason] + +### Timebox [optional] +What's the maximum amount of time that should be spent working on this? + +### Definition of Done +A checklist of things that need to happen in order for this story to be successfully completed +- [ ] Implement and check-in code changes +- [ ] Test code changes +- [ ] Documentation updated (if needed) +- [ ] Code review + +### Subtasks +You can add additional sub-tasks if you'd like to break down the work further. diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml new file mode 100644 index 0000000..daa092a --- /dev/null +++ b/.github/workflows/python_package.yml @@ -0,0 +1,36 @@ +name: Python package + +on: + pull_request: + push: + branches: [ $default-branch ] + + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + pip install .[test] + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest \ No newline at end of file diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..b268138 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,8 @@ +name: Ruff +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d00ecbc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +You should also add project tags for each release in Github, see [Managing releases in a repository](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository). + +## [2.0.0] - 2024-05-29 +### Added +- Added example auto-built Sphinx documentation in the `docs` folder +- Github workflow for running ruff linter +- A note about conda dependencies to README +- A note about using docker containers to README +- Ruff as a linter for development +### Changed +- All build and packaging switched to use only pyproject.toml +- Minimum python version changed to 3.10 +- Github workflow checks python versions 3.10, 3.11, 3.12 +- Updated DVC version to avoid `ImportError: cannot import name 'fsspec_loop'` in older versions +### Removed +- Removed setup.cfg + +## [1.0.0] - 2022-05-23 +### Added +- README and CHANGELOG +- cdstemplate packages for computing word count from input text +- corpus_counter_script.py as a user-facing script with argparse examples +- Tests of cdstemplate packages +- A github workflow to trigger tests on pull request to the main branch +- Sample text data from Project Gutenberg +- Data Version Control stage for the corpus_counter_script.py +- A sample Jupyter notebook that plots most frequent words the Gutenberg data diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md new file mode 100644 index 0000000..94614f5 --- /dev/null +++ b/CONTRIBUTIONS.md @@ -0,0 +1,38 @@ +# Contribution Guidelines +This is a community-driven, open source project that welcomes all contributions. Whether you're a seasoned contributor or new to the project, we're grateful for all contributions. + +## Community standards + +We are an inclusive community that values open dialogue, mutual respect, and fair treatment. Every submission will be treated equally and we encourage those with diverse backgrounds and perspectives to contribute. + +We are part of the University of Massachusetts Amherst, so we adhere to the [UMass Code of Student Conduct](https://www.umass.edu/dean_students/codeofconduct). + +## Getting started +Before contributing to the project, take a look at the README file, which contains information about system requirements, environment setup steps, and a project summary. + +Further documentation for this project is found in the docs folder. + +## Selecting an issue +Issues that are open for contribution are given the following labels: +- good-first-issue + - Issues with this tag are suited for those that do not have previous experience with the project. +- help-wanted + - Issues with this tag are open for contribution and are suited for those with experience in contribution. + +## Submitting contributions + +To contribute to the project, do the following: +- [Fork and clone](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the repository +- Create a [branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository) for your issue +- Make a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) to the main branch of the upstream repository + - Title your pull request with the issue you fixed + - For example, "Fixed upload error to resolve Issue #987" + - Include a short description of the changes you made + +## Issue reporting and help +Report bugs, issues, or suggested features to * insert email.* + +Direct all questions to *insert email *, but keep in mind that we are a small team and may take awhile to respond. + + + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..00a65ef --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 University of Massachusetts Amherst, Center for Data Science + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b131646 --- /dev/null +++ b/README.md @@ -0,0 +1,231 @@ +# PythonProjectTemplate + +This repository presents some opinionated guidelines for creating a data science and machine learning project in Python, using the simple example of scripts for counting words in text documents. By following these guidelines you can make it easy for your code to be tested and understood by others (or yourself months from now), so they can reproduce your experiments. + +These are just guidelines, not strict rules, so feel free to alter them to meet your needs. Just keep in mind the goal is that others can understand and run your code, even if you aren't around to ask questions to! + +This template draws a lot of inspiration from [Cookiecutter Data Science](https://drivendata.github.io/cookiecutter-data-science/). Please read their awesome explanations! + +# Getting Started +## Installing Dependencies and Packages +Use these steps for setting up a development environment to install and work with code in this template: +1) Set up a Python 3 virtual environment using [Conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html#) or [Virtualenv](https://virtualenv.pypa.io/en/latest/index.html). Read [Python Virtual Environments: A Primer](https://realpython.com/python-virtual-environments-a-primer/#the-virtualenv-project) for details on how to get started with virtual environments and why you need them. For a _really detailed_ explanation, see [An unbiased evaluation of environment management and packaging tools](https://alpopkes.com/posts/python/packaging_tools/). +2) Activate your virtual environment. + +3) Install the package. + - If you want to just use the scripts and package features, install the project by running `pip install .` from the root directory. + - If you will be changing the code and running tests, you can install it by running `pip install -e .[test,dev]`. The `-e/--editable` flag means local changes to the project code will always be available with the package is imported. You wouldn't use this in production, but it's useful for development. + + +For example, if you use Conda, you would run the following to create an environment named `template` with python version 3.10, then activate it and install the package in developer mode: +``` +$ conda create -n template python=3.10 -y +Collecting package metadata (current_repodata.json): done +Solving environment: done + +## Package Plan ## + + environment location: /home/virginia/miniconda3/envs/template + + added / updated specs: + - python=3.10 + + + +The following NEW packages will be INSTALLED: + + package | build + ---------------------------|----------------- +... + +$ conda activate `template` +$ pip install -e .[test,dev] +Obtaining file:///home/virginia/workspace/PythonProjectTemplate + Installing build dependencies ... done + Getting requirements to build wheel ... done + Installing backend dependencies ... done + Preparing wheel metadata ... done +Collecting numpy +... +``` + +## Specifying Requirements +In order for users to install your package and all the libraries it depends on by running `pip install`, you need to provide a `pyproject.toml` file. This has two important sections: +- `project`: List project metadata and version information and all library requirements/dependencies, including for testing or development environments. This is the main file you will work with and add requirements to. Some dependencies +- `build-system`: Define the build tool that is used to package and distribute your code. For this project, we use [SetupTools](https://setuptools.pypa.io/en/latest/userguide/quickstart.html). + +If you'd like to learn more about python packaging, refer to [the Python Packaging User Guide](https://packaging.python.org/en/latest/) or [PEP 517](https://peps.python.org/pep-0517/#build-requirements). + +### Requirements via conda environment files +[Anaconda](https://www.anaconda.com/download/) and its bare bones counterpart, [Miniconda](https://docs.anaconda.com/free/miniconda/index.html), are especially useful if your project depends on libraries that are difficult to install in the standard pythonic way, such as [GPU libraries](https://docs.anaconda.com/free/working-with-conda/packages/gpu-packages/). If this is the case, you should also share a [Conda environment file](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-file-manually) with your code. The conda virtual environment will need to be created and activated before any `pip install` steps. Installations with conda dependencies are usually a little more complicated, so make sure you include step-by-step instructions in documentation. + +### Containerized applications +In cases when its important that your software work exactly the same on every operating system or you want to abstract away difficult installation steps for end user, you can consider creating a [Docker container](https://www.docker.com/resources/what-container/). This is often appropriate deploying services in the cloud or providing an application for a tech-savvy person to use on their own. However, it's not necessary for most of our projects. + + +## Directory Structure +So what does each file in this repository do? +``` +. +├── src + ├── cdstemplate # The python package root - Any code you'd like to be able to import lives here +   ├── corpus_counter_script.py # A script that takes a list of documents as input and outputs a CSV of word counts +   ├── __init__.py # Indicates that this directory is a python package, you can put special import instructions here +    ├── word_count.py # A module that has functions and classes to import +   └── utils.py # A module that handles logging and other internals +├── CHANGELOG.md # Versioning information +├── dag_workflow.png # An image that is linked to in this README +├── data # Data files which may or may not be tracked in Git, but we reserve a folder for them so that users can all have the same relative paths +   ├── gutenberg # Sample text input files, the raw inputs to our experiment pipeline. +   └── gutenberg_counts.csv # The expected output file for our experiment. It's generated by `dvc repro` and is ignored by git. +├── docs # Sphinx auto-documentation uses this folder to run its scripts and store documentation +   ├── _build # Contains the Sphinx doctree and html documentation source code + ├── doctrees # A folder with doctree construction information +   └── html # A folder that contains the html code for all automatically created documentation +   ├── _static # A folder that can contain static code +   ├── _templates # A folder that can contain Sphinx templates + ├── conf.py # A function that configures Sphinx according to user specifications + ├── index.rst # A directory that users can input new functions into for auto-documentation + ├── make.bat # A function that runs auto-documentation + └── Makefile # A function that creates html documentation based on functions in the index.rst file +├── dvc.lock # Data Version Control uses this file to compare experiment versions. It's tracked in Git, but don't edit it manually. +├── dvc.yaml # Create the Data Version Control pipeline stages here +├── notebooks +   └── word_count_prototype.ipynb # A jupyter notebook that makes pretty plots +├── pyproject.toml # Project metadata, dependencies and build tools are declared for proper installation and packaging. +├── README.md # You're reading it now! +└── tests + └── test_word_count.py # Unit and smoke tests for the word_count module +├── .dvc # The configuration file for Data Version Control +├── .github + └── workflows/python_package.yml # Github Workflow file, configures running tests on Github every time a pull request to the main branch is made +├── .gitignore # Lists files that should not be included in version control, created from Github's template .gitignore for Python. +└── .dvcignore # Lists files that Data Version Control should skip when checking for changes in stage dependencies. +``` + + +# Communication Tools and Code +When you work with others, it's not just about the code! + +The README, CHANGELOG and docstrings are just as important. + +- _README.md_ : Summarize the project's purpose and give installation instructions. +- _CHANGELOG.md_ : Tell the user what has changed between versions and why, see [Keep A CHANGELOG](https://keepachangelog.com/en/1.0.0/) +- docstrings: Appear directly in your code and give an overview of each function or object. They can be printed using `help(object)` from the python interpreter or used to automatically generate API documentation with a tool like [Sphinx](https://www.sphinx-doc.org/en/master/index.html). There are many different docstring formats. Your team can choose any they like, just be consistent. This template uses [reStructuredText style](https://peps.python.org/pep-0287/). +- Sphinx : Create html documentation for your functions based on the docstrings you write in the code. Use [Sphinx](https://www.sphinx-doc.org/en/master/index.html) to streamline the documentation process. + +Read [Real Python's Documenting Python Code: A Complete Guide](https://realpython.com/documenting-python-code/) for more ideas about effectively documenting code. The `.md` files are written using [Markdown](https://www.markdownguide.org/), a handy formatting language that is automatically rendered in Github. + +# Tests +Although it's [impossible to generally prove that your code is bug-free](https://en.wikipedia.org/wiki/Undecidable_problem), automated testing is a valuable tool. It provides: +- Proof that your code works as intended in most common examples and important edge cases +- Instant feedback on whether changes to the code broke its functionality +- Examples of how to use the code, a type of documentation + +This repository has tests configured using [pytest](https://pytest.org/) and the Github action defined in `.github/workflows/python_package.yml` will run tests every time you make a pull request to the main branch of the repository. [Unittest](https://docs.python.org/3/library/unittest.html#module-unittest) and [nose2](https://docs.nose2.io/en/latest/) are other common test frameworks for python. + +You can run tests locally using `pytest` or `python -m pytest` from the command line from the root of the repository or configure them to be [run with a debugger in your IDE](https://code.visualstudio.com/docs/python/testing). For example: +``` +$ pytest +======================== test session starts ======================== +platform linux -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0 +rootdir: /home/virginia/workspace/PythonProjectTemplate +collected 2 items + +tests/test_sample_module.py . +``` + +Read the following articles for tips on writing your own tests: +- [Getting Started With Testing in Python](https://realpython.com/python-testing/) +- [13 Tips for Writing Useful Unit Tests](https://betterprogramming.pub/13-tips-for-writing-useful-unit-tests-ca20706b5368) +- [Why Good Developers Write Bad Unit Tests](https://mtlynch.io/good-developers-bad-tests) + +# Reproducible Experiments +In practice, data science often relies on pipelining many operations together to prepare data, extract features, then train and evaluate models or produce analysis. Whether someone can reproduce your experiments depends on how clearly you lay out the pipeline and parameters that you use for each 'node' in the pipeline, including stating where to find the input data and how it should be formatted. + +In practice, you should write scripts that are flexible enough to change the parameters you'd like to experiment with and define the pipeline using a directed acyclic graph (DAG), where the outputs from earlier steps become the dependencies for later ones. It's good practice to draw out the DAG for your experiment first, noting inputs, outputs and parameters, before you code scripts for the pipeline, like this: + +![DAG diagram](./dag_workflow.png) + +## Reusable Scripts +Our 'experiment' here is simply counting the occurrence of words from a set of documents, in the form of text files, then writing the counts of each word to a CSV file. This operation is made available to users via the `cdstemplate.corpus_counter_script` and by using the [`argparse` command-line parsing library](https://docs.python.org/3/library/argparse.html#module-argparse), we clearly describe the expected input parameters and options, which can be displayed using the `--help` flag. There are [other command-line parsers](https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/) you can use, but `argparse` comes with python, so you don't need to add an extra requirement. + + +Since we have made the package installable and defined it as the `corpus-counter` script in `project.toml`, users can run it using `corpus-counter`, `python -m cdstemplate.corpus_counter_script` or `python src/cdstemplate/corpus_counter_script.py`, but all work the same way: +``` +$ corpus-counter --help +usage: corpus-counter [-h] [--case-insensitive] csv documents [documents ...] + +A script to generate counts of tokens in a corpus + +positional arguments: + csv Path to the output CSV storing token counts. Required. + documents Paths to at least one raw text document that make up the corpus. Required. + +options: + -h, --help show this help message and exit + --case-insensitive, -c + Default is to have case sensitive tokenization. Use this flag to make the token counting + case insensitive. Optional. +$ python src/cdstemplate/corpus_counter_script.py --help +usage: corpus_counter_script.py [-h] [--case-insensitive] +... +$ python -m cdstemplate.corpus_counter_script --help +usage: corpus_counter_script.py [-h] [--case-insensitive] + csv documents [documents ...] + +A script to generate counts of tokens in a corpus +... +``` + +Using the help message, we can understand how to run the script to count all the words in the text files in `data/gutenberg` in a case-insensitive way, saving word counts to a new csv file, `data/gutenberg_counts.csv`: +``` +$ corpus-counter data/gutenberg_counts.csv data/gutenberg/*.txt --case-insensitive +INFO : 2023-12-08 12:26:10,770 : cdstemplate.corpus_counter_script : Command line arguments: Namespace(csv='data/gutenberg_counts.csv', documents=['data/gutenberg/austen-emma.txt', 'data/gutenberg/austen-persuasion.txt', 'data/gutenberg/austen-sense.txt', 'data/gutenberg/bible-kjv.txt', 'data/gutenberg/blake-poems.txt', 'data/gutenberg/bryant-stories.txt', 'data/gutenberg/burgess-busterbrown.txt', 'data/gutenberg/carroll-alice.txt', 'data/gutenberg/chesterton-ball.txt', 'data/gutenberg/chesterton-brown.txt', 'data/gutenberg/chesterton-thursday.txt'], case_insensitive=True) +DEBUG : 2023-12-08 12:26:10,771 : cdstemplate.word_count : CorpusCounter instantiated, tokenization pattern: \s, case insensitive: True +INFO : 2023-12-08 12:26:10,771 : cdstemplate.corpus_counter_script : Tokenizing document number 0: data/gutenberg/austen-emma.txt +DEBUG : 2023-12-08 12:26:10,771 : cdstemplate.word_count : Tokenizing '[Emma by Jane Austen 1816] +... +``` + +## Data Dependencies Tools +[Build automation tools](https://en.wikipedia.org/wiki/Build_automation) like [Make](https://en.wikipedia.org/wiki/Make_(software)) have been used to resolve dependencies and compile software since the 1970s. Build automation can also be used in data science and machine learning workflows for [many of the same reasons](https://en.wikipedia.org/wiki/Build_automation#Advantages), like eliminating redundant tasks, maintaining history and improved quality and consistency through automating processes. Using a build tool can also be a documentation and communication tool, since it declares the most common ways to run code and reproduce experiments. + +In the Machine Learning Operations (MLOps) community these automation tools are often called [task or workflow orchestration](https://www.datarevenue.com/en-blog/airflow-vs-luigi-vs-argo-vs-mlflow-vs-kubeflow). There are many options, such as [Airflow](https://airflow.apache.org/), [Luigi](https://github.com/spotify/luigi), [MLflow](https://mlflow.org/), [Kubeflow](https://www.kubeflow.org/) and [iterative.ai's DVC and CML](https://iterative.ai/), all with various additional features for versioning experiments, scheduling and visualizations, but at the core they are all built on the same dependency graph principle as the OG [Make](https://opensource.com/article/18/8/what-how-makefile). + +Some of these tools can take a lot of work to set up, so discuss the trade-offs with your team to decide what you'd like to use. In the early stages of a project, we recommend using something easy to set up, like [DVC](https://dvc.org/) or [Make](https://opensource.com/article/18/8/what-how-makefile). + +### DVC Example +In this repository, we have set up a pipeline using [DVC](https://dvc.org/), which has the added benefit of versioning data and experiments. DVC is especially easy to set up for Python projects, because it can be installed via pip in the project requirements and integrates with git. See [DVC Get Started documentation](https://dvc.org/doc/start) for instructions on setting up DVC in your own repository. + +The stages in our word count experiment pipeline are configured in `dvc.yaml`. As described in the previous section, this takes the `data/gutenberg` files as input and produces `data/gutenberg_counts.csv` as the final product. Since `data/gutenberg_counts.csv` should be generated whenever the data or scripts change, it is managed by DVC and ignored by git. You can re-run the pipeline steps by running `dvc repro`. +``` +$ dvc repro +Running stage 'count-words': +> python cdstemplate/corpus_counter_script.py data/gutenberg_counts.csv data/gutenberg/*.txt --case-insensitive +INFO : 2022-05-23 11:18:42,813 : __main__ : Command line arguments: Namespace(csv='data/gutenberg_counts.csv', documents=['data/gutenberg/austen-emma.txt', 'data/gutenberg/austen-persuasion.txt', 'data/gutenberg/austen-sense.txt', 'data/gutenberg/bible-kjv.txt', 'data/gutenberg/blake-poems.txt', 'data/gutenberg/bryant-stories.txt', 'data/gutenberg/burgess-busterbrown.txt', 'data/gutenberg/carroll-alice.txt', 'data/gutenberg/chesterton-ball.txt', 'data/gutenberg/chesterton-brown.txt', 'data/gutenberg/chesterton-thursday.txt'], case_insensitive=True) +... +$ dvc repro +Stage 'count-words' didn't change, skipping +Data and pipelines are up to date. +``` + + +You can see the stages in the DAG by running `dvc dag`, in our case it's just a single step called `count-words`: +``` +$ dvc dag ++-------------+ +| count-words | ++-------------+ +``` + +## A Note on Notebooks +We have also included an example Jupyter notebook + +Jupyter notebooks are useful tools for exploratory data analysis, prototyping baseline models and creating visualizations. However, they are _not_ an acceptable way to hand-off code for others to reproduce. Have you ever tried to run someone else's notebook, only to find out a cell was deleted, and you have no idea what it was supposed to do? + +[Don't put data science notebooks into production](https://martinfowler.com/articles/productize-data-sci-notebooks.html), they are [hard to test, version, parametrize and keep track of state](https://www.reddit.com/r/datascience/comments/ezh50g/jupyter_notebooks_in_productionno_just_no/). + +There _are_ [companies that use notebooks in production architecture](https://blog.goodaudience.com/inside-netflixs-notebook-driven-architecture-aedded32145e), but they have entire Devops organizations to help configure deployment and _still_ use workflow tools like [papermill](https://papermill.readthedocs.io/en/latest/) and Airflow to parametrize notebook dependencies. Unless you are willing to put in the effort to parametrize your notebooks in pipeline workflows, don't use them when stability and reproducibility matter. + +Best practices for working with notebooks are changing as they become more popular. However, for now most of these services are too expensive for our partners or difficult to configure. You can use a notebook for prototyping and exploratory analysis, but once the project moves forward, use [`nbconvert`](https://linuxhint.com/convert-jupyter-notebook-python/) to convert the notebook to python code, then add some tests! \ No newline at end of file diff --git a/dag_workflow.png b/dag_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..129f78d4453d05f6f6b29afc94658137f00fcbe6 GIT binary patch literal 20788 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 RTL1$g=BCyr`6u0O{a?@Fj(q?C literal 0 HcmV?d00001 diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..ff8bf23 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1 @@ +/gutenberg_counts.csv diff --git a/data/gutenberg/austen-emma.txt b/data/gutenberg/austen-emma.txt new file mode 100644 index 0000000..44c118e --- /dev/null +++ b/data/gutenberg/austen-emma.txt @@ -0,0 +1,25 @@ +[Emma by Jane Austen 1816] + +VOLUME I + +CHAPTER I + + +Emma Woodhouse, handsome, clever, and rich, with a comfortable home +and happy disposition, seemed to unite some of the best blessings +of existence; and had lived nearly twenty-one years in the world +with very little to distress or vex her. + +She was the youngest of the two daughters of a most affectionate, +indulgent father; and had, in consequence of her sister's marriage, +been mistress of his house from a very early period. Her mother +had died too long ago for her to have more than an indistinct +remembrance of her caresses; and her place had been supplied +by an excellent woman as governess, who had fallen little short +of a mother in affection. + +Sixteen years had Miss Taylor been in Mr. Woodhouse's family, +less as a governess than a friend, very fond of both daughters, +but particularly of Emma. Between _them_ it was more the intimacy +of sisters. Even before Miss Taylor had ceased to hold the nominal +office of governess, the mildness o \ No newline at end of file diff --git a/data/gutenberg/austen-persuasion.txt b/data/gutenberg/austen-persuasion.txt new file mode 100644 index 0000000..356fa8b --- /dev/null +++ b/data/gutenberg/austen-persuasion.txt @@ -0,0 +1,24 @@ +[Persuasion by Jane Austen 1818] + + +Chapter 1 + + +Sir Walter Elliot, of Kellynch Hall, in Somersetshire, was a man who, +for his own amusement, never took up any book but the Baronetage; +there he found occupation for an idle hour, and consolation in a +distressed one; there his faculties were roused into admiration and +respect, by contemplating the limited remnant of the earliest patents; +there any unwelcome sensations, arising from domestic affairs +changed naturally into pity and contempt as he turned over +the almost endless creations of the last century; and there, +if every other leaf were powerless, he could read his own history +with an interest which never failed. This was the page at which +the favourite volume always opened: + + "ELLIOT OF KELLYNCH HALL. + +"Walter Elliot, born March 1, 1760, married, July 15, 1784, Elizabeth, +daughter of James Stevenson, Esq. of South Park, in the county of +Gloucester, by which lady (who died 1800) he has issue Elizabeth, +born June 1, 1785; Ann \ No newline at end of file diff --git a/data/gutenberg/austen-sense.txt b/data/gutenberg/austen-sense.txt new file mode 100644 index 0000000..b958d68 --- /dev/null +++ b/data/gutenberg/austen-sense.txt @@ -0,0 +1,22 @@ +[Sense and Sensibility by Jane Austen 1811] + +CHAPTER 1 + + +The family of Dashwood had long been settled in Sussex. +Their estate was large, and their residence was at Norland Park, +in the centre of their property, where, for many generations, +they had lived in so respectable a manner as to engage +the general good opinion of their surrounding acquaintance. +The late owner of this estate was a single man, who lived +to a very advanced age, and who for many years of his life, +had a constant companion and housekeeper in his sister. +But her death, which happened ten years before his own, +produced a great alteration in his home; for to supply +her loss, he invited and received into his house the family +of his nephew Mr. Henry Dashwood, the legal inheritor +of the Norland estate, and the person to whom he intended +to bequeath it. In the society of his nephew and niece, +and their children, the old Gentleman's days were +comfortably spent. His attachment to them all increased. +The constant attention \ No newline at end of file diff --git a/data/gutenberg/bible-kjv.txt b/data/gutenberg/bible-kjv.txt new file mode 100644 index 0000000..7a8ae82 --- /dev/null +++ b/data/gutenberg/bible-kjv.txt @@ -0,0 +1,32 @@ +[The King James Bible] + +The Old Testament of the King James Bible + +The First Book of Moses: Called Genesis + + +1:1 In the beginning God created the heaven and the earth. + +1:2 And the earth was without form, and void; and darkness was upon +the face of the deep. And the Spirit of God moved upon the face of the +waters. + +1:3 And God said, Let there be light: and there was light. + +1:4 And God saw the light, that it was good: and God divided the light +from the darkness. + +1:5 And God called the light Day, and the darkness he called Night. +And the evening and the morning were the first day. + +1:6 And God said, Let there be a firmament in the midst of the waters, +and let it divide the waters from the waters. + +1:7 And God made the firmament, and divided the waters which were +under the firmament from the waters which were above the firmament: +and it was so. + +1:8 And God called the firmament Heaven. And the evening and the +morning were the second day. + +1:9 And God said, Let the waters under the heav \ No newline at end of file diff --git a/data/gutenberg/blake-poems.txt b/data/gutenberg/blake-poems.txt new file mode 100644 index 0000000..7ebd928 --- /dev/null +++ b/data/gutenberg/blake-poems.txt @@ -0,0 +1,47 @@ +[Poems by William Blake 1789] + + +SONGS OF INNOCENCE AND OF EXPERIENCE +and THE BOOK of THEL + + + SONGS OF INNOCENCE + + + INTRODUCTION + + Piping down the valleys wild, + Piping songs of pleasant glee, + On a cloud I saw a child, + And he laughing said to me: + + "Pipe a song about a Lamb!" + So I piped with merry cheer. + "Piper, pipe that song again;" + So I piped: he wept to hear. + + "Drop thy pipe, thy happy pipe; + Sing thy songs of happy cheer:!" + So I sang the same again, + While he wept with joy to hear. + + "Piper, sit thee down and write + In a book, that all may read." + So he vanish'd from my sight; + And I pluck'd a hollow reed, + + And I made a rural pen, + And I stain'd the water clear, + And I wrote my happy songs + Every child may joy to hear. + + + THE SHEPHERD + + How sweet is the Shepherd's sweet lot! + From the morn to the evening he stays; + He shall follow his sheep all the day, + And his tongue shall be filled with praise. + + For he hears the lambs' innocent call, + And \ No newline at end of file diff --git a/data/gutenberg/bryant-stories.txt b/data/gutenberg/bryant-stories.txt new file mode 100644 index 0000000..e07d807 --- /dev/null +++ b/data/gutenberg/bryant-stories.txt @@ -0,0 +1,40 @@ +[Stories to Tell to Children by Sara Cone Bryant 1918] + + +TWO LITTLE RIDDLES IN RHYME + + + There's a garden that I ken, + Full of little gentlemen; + Little caps of blue they wear, + And green ribbons, very fair. + (Flax.) + + From house to house he goes, + A messenger small and slight, + And whether it rains or snows, + He sleeps outside in the night. + (The path.) + + + + +THE LITTLE YELLOW TULIP + + +Once there was a little yellow Tulip, and she lived down in a little +dark house under the ground. One day she was sitting there, all by +herself, and it was very still. Suddenly, she heard a little _tap, tap, +tap_, at the door. + +"Who is that?" she said. + +"It's the Rain, and I want to come in," said a soft, sad, little voice. + +"No, you can't come in," the little Tulip said. + +By and by she heard another little _tap, tap, tap_ on the window-pane. + +"Who is there?" she said. + +The same soft little voice answered, "It's the \ No newline at end of file diff --git a/data/gutenberg/burgess-busterbrown.txt b/data/gutenberg/burgess-busterbrown.txt new file mode 100644 index 0000000..ff5e628 --- /dev/null +++ b/data/gutenberg/burgess-busterbrown.txt @@ -0,0 +1,22 @@ +[The Adventures of Buster Bear by Thornton W. Burgess 1920] + +I + +BUSTER BEAR GOES FISHING + + +Buster Bear yawned as he lay on his comfortable bed of leaves and +watched the first early morning sunbeams creeping through the Green +Forest to chase out the Black Shadows. Once more he yawned, and slowly +got to his feet and shook himself. Then he walked over to a big +pine-tree, stood up on his hind legs, reached as high up on the trunk of +the tree as he could, and scratched the bark with his great claws. After +that he yawned until it seemed as if his jaws would crack, and then sat +down to think what he wanted for breakfast. + +While he sat there, trying to make up his mind what would taste best, he +was listening to the sounds that told of the waking of all the little +people who live in the Green Forest. He heard Sammy Jay way off in the +distance screaming, "Thief! Thief!" and grinned. "I wonder," thought +Buster, "if some one has stolen Sammy's breakfast, or if he has stolen +th \ No newline at end of file diff --git a/data/gutenberg/carroll-alice.txt b/data/gutenberg/carroll-alice.txt new file mode 100644 index 0000000..da72958 --- /dev/null +++ b/data/gutenberg/carroll-alice.txt @@ -0,0 +1,21 @@ +[Alice's Adventures in Wonderland by Lewis Carroll 1865] + +CHAPTER I. Down the Rabbit-Hole + +Alice was beginning to get very tired of sitting by her sister on the +bank, and of having nothing to do: once or twice she had peeped into the +book her sister was reading, but it had no pictures or conversations in +it, 'and what is the use of a book,' thought Alice 'without pictures or +conversation?' + +So she was considering in her own mind (as well as she could, for the +hot day made her feel very sleepy and stupid), whether the pleasure +of making a daisy-chain would be worth the trouble of getting up and +picking the daisies, when suddenly a White Rabbit with pink eyes ran +close by her. + +There was nothing so VERY remarkable in that; nor did Alice think it so +VERY much out of the way to hear the Rabbit say to itself, 'Oh dear! +Oh dear! I shall be late!' (when she thought it over afterwards, it +occurred to her that she ought to have wondered at this, but at the time +it all seemed quite natural); but \ No newline at end of file diff --git a/data/gutenberg/chesterton-ball.txt b/data/gutenberg/chesterton-ball.txt new file mode 100644 index 0000000..7efd363 --- /dev/null +++ b/data/gutenberg/chesterton-ball.txt @@ -0,0 +1,21 @@ +[The Ball and The Cross by G.K. Chesterton 1909] + + +I. A DISCUSSION SOMEWHAT IN THE AIR + +The flying ship of Professor Lucifer sang through the skies like +a silver arrow; the bleak white steel of it, gleaming in the +bleak blue emptiness of the evening. That it was far above the +earth was no expression for it; to the two men in it, it seemed +to be far above the stars. The professor had himself invented +the flying machine, and had also invented nearly everything in +it. Every sort of tool or apparatus had, in consequence, to the +full, that fantastic and distorted look which belongs to the +miracles of science. For the world of science and evolution is +far more nameless and elusive and like a dream than the world of +poetry and religion; since in the latter images and ideas remain +themselves eternally, while it is the whole idea of evolution +that identities melt into each other as they do in a nightmare. + +All the tools of Professor Lucifer were the ancient human tools +gone mad, grown into \ No newline at end of file diff --git a/data/gutenberg/chesterton-brown.txt b/data/gutenberg/chesterton-brown.txt new file mode 100644 index 0000000..85c5d03 --- /dev/null +++ b/data/gutenberg/chesterton-brown.txt @@ -0,0 +1,20 @@ +[The Wisdom of Father Brown by G. K. Chesterton 1914] + + +I. The Absence of Mr Glass + + +THE consulting-rooms of Dr Orion Hood, the eminent criminologist +and specialist in certain moral disorders, lay along the sea-front +at Scarborough, in a series of very large and well-lighted french windows, +which showed the North Sea like one endless outer wall of blue-green marble. +In such a place the sea had something of the monotony of a blue-green dado: +for the chambers themselves were ruled throughout by a terrible tidiness +not unlike the terrible tidiness of the sea. It must not be supposed +that Dr Hood's apartments excluded luxury, or even poetry. +These things were there, in their place; but one felt that +they were never allowed out of their place. Luxury was there: +there stood upon a special table eight or ten boxes of the best cigars; +but they were built upon a plan so that the strongest were always +nearest the wall and the mildest nearest the window. A tantalus +containing three kinds of sp \ No newline at end of file diff --git a/data/gutenberg/chesterton-thursday.txt b/data/gutenberg/chesterton-thursday.txt new file mode 100644 index 0000000..e44bd92 --- /dev/null +++ b/data/gutenberg/chesterton-thursday.txt @@ -0,0 +1,20 @@ +[The Man Who Was Thursday by G. K. Chesterton 1908] + +To Edmund Clerihew Bentley + +A cloud was on the mind of men, and wailing went the weather, +Yea, a sick cloud upon the soul when we were boys together. +Science announced nonentity and art admired decay; +The world was old and ended: but you and I were gay; +Round us in antic order their crippled vices came-- +Lust that had lost its laughter, fear that had lost its shame. +Like the white lock of Whistler, that lit our aimless gloom, +Men showed their own white feather as proudly as a plume. +Life was a fly that faded, and death a drone that stung; +The world was very old indeed when you and I were young. +They twisted even decent sin to shapes not to be named: +Men were ashamed of honour; but we were not ashamed. +Weak if we were and foolish, not thus we failed, not thus; +When that black Baal blocked the heavens he had no hymns from us +Children we were--our forts of sand were even as weak as eve, +High as they went we piled them up to break that b \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..00e9eb6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,55 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'testdoc' +copyright = '2023, Luke Ruud' +author = 'Luke Ruud' + +# The full version, including alpha/beta/rc tags +release = '0.1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon" +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7024bb2 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,24 @@ +.. testdoc documentation master file, created by + sphinx-quickstart on Mon Jul 24 13:17:26 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to testdoc's documentation! +=================================== + + +.. automodule:: cdstemplate.word_count + :members: + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/dvc.lock b/dvc.lock new file mode 100644 index 0000000..aa51e59 --- /dev/null +++ b/dvc.lock @@ -0,0 +1,18 @@ +schema: '2.0' +stages: + count-words: + cmd: python src/cdstemplate/corpus_counter_script.py data/gutenberg_counts.csv + data/gutenberg/*.txt --case-insensitive + deps: + - path: data/gutenberg + md5: 41d960155f1a7f55480c03cea68ba2a7.dir + size: 10940 + nfiles: 11 + - path: src/cdstemplate/corpus_counter_script.py + hash: md5 + md5: a4bb400c0cfd7050ac4b761b550a0a56 + size: 2582 + outs: + - path: data/gutenberg_counts.csv + md5: 74abc508b4e4015ab4136405df251a57 + size: 4922 diff --git a/dvc.yaml b/dvc.yaml new file mode 100644 index 0000000..ed00753 --- /dev/null +++ b/dvc.yaml @@ -0,0 +1,8 @@ +stages: + count-words: + cmd: python src/cdstemplate/corpus_counter_script.py data/gutenberg_counts.csv data/gutenberg/*.txt --case-insensitive + deps: + - src/cdstemplate/corpus_counter_script.py + - data/gutenberg + outs: + - data/gutenberg_counts.csv \ No newline at end of file diff --git a/notebooks/word_count_prototype.ipynb b/notebooks/word_count_prototype.ipynb new file mode 100644 index 0000000..25fb9a2 --- /dev/null +++ b/notebooks/word_count_prototype.ipynb @@ -0,0 +1,372 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Word Count Prototype\n", + "This notebook presents a prototype of our word count experiment example. It calls our `cdstemplate` library directly, to avoid reduplicating code, then creates a plot of the most frequent words in the corpus. Notebooks are a great way to create visualizations, which often need to be tweaked for readability and aesthetics. \n", + "\n", + "Don't forget to restart your kernel and re-run the notebook completely before you commit or share it with others! This helps avoid problems arising from deleted or reordered cells." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# This cell imports packages and defines any experimental set-up \n", + "# Try to make experimental parameters easy for others to to find by including them in a few cells at the beginning of your notebook\n", + "import logging\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "from cdstemplate import word_count\n", + "\n", + "# You can include this to see log messages from the packages you're using\n", + "logging.basicConfig(level=logging.INFO)\n", + "\n", + "# A relative path to the input data\n", + "input_txt_dir = \"../data/gutenberg\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:cdstemplate.word_count:Adding 181 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 122 word types\n", + "INFO:cdstemplate.word_count:Adding 189 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 105 word types\n", + "INFO:cdstemplate.word_count:Adding 180 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 92 word types\n", + "INFO:cdstemplate.word_count:Adding 192 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 95 word types\n", + "INFO:cdstemplate.word_count:Adding 246 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 83 word types\n", + "INFO:cdstemplate.word_count:Adding 189 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 83 word types\n", + "INFO:cdstemplate.word_count:Adding 258 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 82 word types\n", + "INFO:cdstemplate.word_count:Adding 204 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 57 word types\n", + "INFO:cdstemplate.word_count:Adding 197 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 90 word types\n", + "INFO:cdstemplate.word_count:Adding 180 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 74 word types\n", + "INFO:cdstemplate.word_count:Adding 182 token(s) case insensitively\n", + "INFO:cdstemplate.word_count:Vocabulary size increased by 66 word types\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tokenizing file: ../data/gutenberg/austen-persuasion.txt\n", + "Tokenizing file: ../data/gutenberg/chesterton-ball.txt\n", + "Tokenizing file: ../data/gutenberg/chesterton-brown.txt\n", + "Tokenizing file: ../data/gutenberg/carroll-alice.txt\n", + "Tokenizing file: ../data/gutenberg/bryant-stories.txt\n", + "Tokenizing file: ../data/gutenberg/burgess-busterbrown.txt\n", + "Tokenizing file: ../data/gutenberg/blake-poems.txt\n", + "Tokenizing file: ../data/gutenberg/bible-kjv.txt\n", + "Tokenizing file: ../data/gutenberg/chesterton-thursday.txt\n", + "Tokenizing file: ../data/gutenberg/austen-sense.txt\n", + "Tokenizing file: ../data/gutenberg/austen-emma.txt\n" + ] + } + ], + "source": [ + "# Add counts for each document\n", + "corpus_counter = word_count.CorpusCounter()\n", + "for txt_file in Path(input_txt_dir).glob(\"*.txt\"):\n", + " print(\"Tokenizing file:\", txt_file)\n", + " txt_contents = txt_file.read_text()\n", + " corpus_counter.add_doc(txt_contents)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vocab size: 949\n" + ] + } + ], + "source": [ + "# How many unique words appeared in our corpus?\n", + "print(\"Vocab size:\", corpus_counter.get_vocab_size())" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tokencount
616\"Drop1
91\"ELLIOT1
571\"I1
475\"It's2
484\"No,1
\n", + "
" + ], + "text/plain": [ + " token count\n", + "616 \"Drop 1\n", + "91 \"ELLIOT 1\n", + "571 \"I 1\n", + "475 \"It's 2\n", + "484 \"No, 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Get the dataframe we'll work with for the display\n", + "word_count_df = corpus_counter.get_token_counts_as_dataframe()\n", + "display(word_count_df.head())" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tokencount
29the127
10of72
38and62
16a43
162to38
.........
259one3
348what3
376when3
704firmament3
147white3
\n", + "

100 rows × 2 columns

\n", + "
" + ], + "text/plain": [ + " token count\n", + "29 the 127\n", + "10 of 72\n", + "38 and 62\n", + "16 a 43\n", + "162 to 38\n", + ".. ... ...\n", + "259 one 3\n", + "348 what 3\n", + "376 when 3\n", + "704 firmament 3\n", + "147 white 3\n", + "\n", + "[100 rows x 2 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# We only want to include to top 100 most frequent words in our plot\n", + "top_words_df = word_count_df.sort_values(\"count\", ascending=False).head(100)\n", + "display(top_words_df)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Top 100 Most Frequent Words in a Subset of Project Gutenberg Texts')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Make the plot and customize how it is displayed\n", + "plt.figure(figsize= (20, 15))\n", + "fig = sns.barplot(data = top_words_df, x = \"count\", y = \"token\")\n", + "fig.set_title(\"Top 100 Most Frequent Words in a Subset of Project Gutenberg Texts\")" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "8db04dca66b9396af2474eca4189d3a8ab65d348a7a173a34f354ffe25d5d9d4" + }, + "kernelspec": { + "display_name": "Python 3.10.4 ('template')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1d8a32b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +# A build system is required to convert your code into a distributable package. +# setuptools is the oldest and most common build tool, but we also like Poetry +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "cdstemplate" +version = "2.0.0" +description = "A template repo for data science and machine learning projects at UMass Center for Data Science." +readme = "README.md" + +# What version of python does your library work with? +requires-python = ">=3.10" + +# Metadata about your package in case you upload it to PYPI +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +# All the dependencies needed for running your module go here +dependencies = [ + "dvc>=2.42.0", + "numpy", + "pandas", + "scikit-learn", +] + +[project.optional-dependencies] +# Extra dependencies only needed for running tests go here +test = ["pytest"] + +# Dependencies that are useful only to developers, like an autoformatter and support for visualizations in jupyter notebooks go here +dev = [ + "ruff", + "jupyter", + "matplotlib", + "seaborn", + "sphinx", +] + +# If your project contains scripts you'd like to be available command line, you can define them here. +# The value must be of the form ":." +[project.scripts] +corpus-counter = "cdstemplate:corpus_counter_script.main_cli" diff --git a/src/cdstemplate/__init__.py b/src/cdstemplate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cdstemplate/corpus_counter_script.py b/src/cdstemplate/corpus_counter_script.py new file mode 100644 index 0000000..404550d --- /dev/null +++ b/src/cdstemplate/corpus_counter_script.py @@ -0,0 +1,68 @@ +"""An example of a script you can run. It tokenizes an folder of input documents and +writes the corpus counts to a user-specified CSV file +""" +# Import modules, functions and classes from external libraries +import argparse +import logging +from pathlib import Path + +# Import the code from this project needed for this script +from cdstemplate import word_count, utils + +logger = logging.getLogger(__name__) + +def main_cli(): + """A wrapper function that defines command line arguments and help messages for + when the user wants run this module's code as a script. + """ + # The argument parser gives nice ways to include help message and specify which arguments + # are required or optional, see https://docs.python.org/3/library/argparse.html#prog for usage instructions + parser = argparse.ArgumentParser( + description="A script to generate counts of tokens in a corpus" + ) + + parser.add_argument( + "csv", help="Path to the output CSV storing token counts. Required." + ) + + parser.add_argument( + "documents", + nargs="+", + help="Paths to at least one raw text document that make up the corpus. Required.", + ) + parser.add_argument( + "--case-insensitive", + "-c", + action="store_true", + help="Default is to have case sensitive tokenization. Use this flag to make the token counting case insensitive. Optional.", + ) + + args = parser.parse_args() + utils.configure_logging() + logger.info("Command line arguments: %s", args) + main(args.csv, args.documents, args.case_insensitive) + + +def main(csv_out, documents, case_insensitive=False): + """Determine cumulative word counts for a list of documents and write the results to a CSV file + + :param csv_out: output CSV file path + :type csv_out: str or Path + :param documents: list of paths to documents to parse word counts from + :type documents: list of str + :param case_insensitive: Set to True to lowercase all words in cumulative counts, defaults to False + :type case_insensitive: bool, optional + """ + cc = word_count.CorpusCounter(case_insensitive=case_insensitive) + for i, doc in enumerate(documents): + if i % 2 == 0: + logger.info("Tokenizing document number %s: %s", i, doc) + cc.add_doc(Path(doc).read_text()) + + cc.save_token_counts(csv_out) + + +# The entry point of your script - if a user runs it from the command line, for example using `python -m .` +# or `python .py`, this is what will be run. +if __name__ == "__main__": + main_cli() diff --git a/src/cdstemplate/utils.py b/src/cdstemplate/utils.py new file mode 100644 index 0000000..320e122 --- /dev/null +++ b/src/cdstemplate/utils.py @@ -0,0 +1,12 @@ +"""A module for important set-up and configuration functionality, but doesn't implement the library's key features. +""" +import logging + + +def configure_logging(): + """A helper method that configures logging, usable by any script in this library. + """ + logging.basicConfig( + level=logging.DEBUG, + format="%(levelname)s : %(asctime)s : %(name)s : %(message)s", + ) diff --git a/src/cdstemplate/word_count.py b/src/cdstemplate/word_count.py new file mode 100644 index 0000000..8037dce --- /dev/null +++ b/src/cdstemplate/word_count.py @@ -0,0 +1,114 @@ +"""An example of an module with functions and a class that can be imported once the package is installed. +This module provides operations for tokenization and tracking cumulative word counts in a set of documents. +""" +from collections import Counter +import logging +import re + +import pandas as pd + +# You should use logging instead of print statements in code others will use, +# so they can customize how much detail to see from your package +# Refer to https://realpython.com/python-logging/ for detailed examples. +logger = logging.getLogger(__name__) + + +def tokenize(text, pattern=r"\s"): + """Returns a list of strings, the text split into tokens based on the regex pattern to identify boundaries. + + :param text: the document to tokenize + :type text: str + :param pattern: regex string to split the text on + :type pattern: str + """ + logger.debug("Tokenizing '%s' with pattern '%s'", text, pattern) + + tokenized = re.split(pattern, text) + logger.debug("%s token(s) found.", len(tokenized)) + return tokenized + + +class CorpusCounter: + """A simple class object that tracks document and token counts in a corpus. + """ + + def __init__(self, tokenization_pattern=r"\s", case_insensitive=False): + """Constructor instantiates with empty counters + + :param tokenization_pattern: An optional tokenization pattern so that you are consistently tokenizing all documents the same. Defaults to splitting on whitespace + :param case_insensitive: Set to True to downcase tokens before counting, defaults to False + """ + self.token_counter = Counter() + self.doc_counter = 0 + self.tokenization_pattern = tokenization_pattern + self.case_insensitive = case_insensitive + logger.debug( + "CorpusCounter instantiated, tokenization pattern: %s, case insensitive: %s", + tokenization_pattern, + case_insensitive, + ) + + def add_tokenized_doc(self, token_list): + """Tallies an already tokenized document in the corpus. + + :param token_list: A tokenized document + :type token_list: list or iterable of strings + """ + before_vocab_size = self.get_vocab_size() + non_empty_tokens = [w for w in token_list if w != ""] + if self.case_insensitive: + logger.info("Adding %s token(s) case insensitively", len(token_list)) + self.token_counter.update([w.lower() for w in non_empty_tokens]) + else: + logger.info("Adding %s token(s) case insensitively", len(token_list)) + self.token_counter.update(non_empty_tokens) + after_vocab_size = self.get_vocab_size() + + logger.info( + "Vocabulary size increased by %s word types", + after_vocab_size - before_vocab_size, + ) + + self.doc_counter += 1 + + def add_doc(self, untokenized_doc): + """Tokenizes a document and adds it in the corpus. + + :param untokenized_doc: The document to count tokens for + :type untokenized_doc: str + """ + tokenized = tokenize(untokenized_doc, self.tokenization_pattern) + self.add_tokenized_doc(tokenized) + + def get_token_count(self, token): + """Returns the count of a given token in the corpus + + :param token: The token to retrieve counts of + :type token: str + """ + return self.token_counter[token] + + def get_vocab_size(self): + """Returns vocabulary size (number of unique tokens) + """ + return len(self.token_counter) + + def get_token_counts_as_dataframe(self): + """Returns the token counts of the corpus as a Pandas DataFrame with columns 'token', 'count' + """ + dataframe = pd.DataFrame.from_records( + list(self.token_counter.items()), columns=["token", "count"] + ) + dataframe = dataframe.sort_values("token") + return dataframe + + def save_token_counts(self, csv_file): + """Saves the counts of tokens the corpus to a specified + CSV file in alphabetical order + + :param csv_file: Path to desired CSV output file + :type csv_file: str or Path + """ + logger.info("Saving token counts to %s", csv_file) + self.get_token_counts_as_dataframe().to_csv(csv_file, index=False, header=True) + diff --git a/tests/test_word_count.py b/tests/test_word_count.py new file mode 100644 index 0000000..aa91336 --- /dev/null +++ b/tests/test_word_count.py @@ -0,0 +1,105 @@ +"""Tests for the cdstemplate.word_count methods and classes. + +In pytest, each individual test is a python function that starts with `test`. +""" +# Import your library for testing +from cdstemplate import word_count + + +def test_tokenize_document(): + my_document = "It was all very well to say `Drink me,' but the wise little Alice was not going to do that in a hurry." + + expected_tokens = [ + "It", + "was", + "all", + "very", + "well", + "to", + "say", + "`Drink", + "me,'", + "but", + "the", + "wise", + "little", + "Alice", + "was", + "not", + "going", + "to", + "do", + "that", + "in", + "a", + "hurry.", + ] + + assert word_count.tokenize(my_document) == expected_tokens + + +def test_tokenize_change_pattern(): + formatted_document = "here's-a-document-with-strange-formatting" + expected_tokens = ["here's", "a", "document", "with", "strange", "formatting"] + assert word_count.tokenize(formatted_document, pattern="-") == expected_tokens + + +def test_corpus_counter_init(): + cc = word_count.CorpusCounter() + assert cc.doc_counter == 0 + assert cc.get_token_count("word") == 0 + assert not cc.case_insensitive + assert cc.tokenization_pattern == r"\s" + + +def test_corpus_counter_add_docs(): + cc = word_count.CorpusCounter() + cc.add_doc("a b a word") + assert cc.doc_counter == 1 + assert cc.get_token_count("a") == 2 + assert cc.get_token_count("b") == 1 + assert cc.get_token_count("word") == 1 + cc.add_tokenized_doc(["Word", "word", "b"]) + assert cc.get_token_count("a") == 2 + assert cc.get_token_count("b") == 2 + assert cc.get_token_count("word") == 2 + assert cc.get_token_count("Word") == 1 + + +def test_corpus_counter_add_empty_doc(): + cc = word_count.CorpusCounter() + cc.add_doc("") + assert cc.doc_counter == 1 + assert len(cc.token_counter) == 0 + + +def test_corpus_counter_case_insensitive(): + cc = word_count.CorpusCounter(case_insensitive=True) + cc.add_doc("A a B b") + assert cc.get_token_count("a") == 2 + assert cc.get_token_count("b") == 2 + assert cc.get_token_count("A") == 0 + assert cc.get_token_count("B") == 0 + + +def test_corpus_counter_to_dataframe(): + cc = word_count.CorpusCounter() + cc.add_doc("A a B b") + dataframe = cc.get_token_counts_as_dataframe() + assert dataframe.shape == (4, 2) + assert list(dataframe.columns) == ["token", "count"] + assert set(dataframe["token"]) == set(["A", "a", "B", "b"]) + + +# The tmp_path fixture allows you save results to a temporary directory +# that will automatically be cleaned up by the OS later +def test_corpus_counter_save_csv(tmp_path): + my_csv = tmp_path / "token_count.csv" + cc = word_count.CorpusCounter() + cc.add_doc("a b c") + cc.add_doc("a x y z") + cc.save_token_counts(my_csv) + assert my_csv.exists() + assert my_csv.is_file() + expected_csv = "token,count\na,2\nb,1\nc,1\nx,1\ny,1\nz,1\n" + assert my_csv.read_text() == expected_csv