diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..663df7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +*.egg-info + +# Ignore because we're using this as a library. +uv.lock diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8d8806e --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +install: ## Install as library + uv sync + uv pip install -e . + +run: ## Run the example script + uv run python polislite/polislite.py + + +%: + @true + +.PHONY: help + +help: + @echo 'Usage: make ' + @echo + @echo 'where is one of the following:' + @echo + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index 6ee5fea..31317ee 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,35 @@ A lightweight Pol.is-like. ## Setup - pip install scikit-learn + - [Install][install-uv] `uv` Python package manager + + [install-uv]: https://docs.astral.sh/uv/getting-started/installation/ ## Usage - python polislite.py +This repo can be run as a self-contained example script, or used as a library. + +### As A Library + +This package can be installed as a library in another Python project using any package manager. + + pip install git+https://github.com/patcon/polislite.git@python-package + + uv add git+https://github.com/patcon/polislite.git@python-package + +This also makes it simple to use in a Jupyter Notebook. + +See sample notebook: [`polislite_library_usage.ipynb`][ipynb-example] + + [ipynb-example]: /polislite_library_usage.ipynb + +### Example script + + uv run python polislite/polislite.py + +### Development + +Run `make` to see shortcut tasks for working on this project.
Output diff --git a/polislite/__init__.py b/polislite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polislite.py b/polislite/polislite.py similarity index 77% rename from polislite.py rename to polislite/polislite.py index 9b04e3a..304f9cc 100644 --- a/polislite.py +++ b/polislite/polislite.py @@ -99,31 +99,32 @@ def _generate_report(self, vote_matrix, clusters, statements): stance = 'strongly agrees with' if opinion > 0 else 'strongly disagrees with' print(f'- {stance}: {stmt}') -# Example usage -statements = [ - 'Climate change requires immediate action', - 'Nuclear power is necessary for clean energy', - 'Carbon tax should be implemented globally', - 'Individual actions matter for sustainability', - 'Companies should be held liable for emissions' -] +if __name__ == "__main__": + # Example usage + statements = [ + 'Climate change requires immediate action', + 'Nuclear power is necessary for clean energy', + 'Carbon tax should be implemented globally', + 'Individual actions matter for sustainability', + 'Companies should be held liable for emissions' + ] -votes = [ - # Group 1: Environmental purists (anti-nuclear) - ['agree', 'disagree', 'agree', 'agree', 'agree'], - ['agree', 'disagree', 'agree', 'agree', 'agree'], - ['agree', 'disagree', 'agree', 'agree', 'agree'], - - # Group 2: Tech-focused environmentalists (pro-nuclear) - ['agree', 'agree', 'agree', 'disagree', 'agree'], - ['agree', 'agree', 'agree', 'disagree', 'agree'], - ['agree', 'agree', 'agree', 'disagree', 'agree'], - - # Group 3: Business-oriented (anti-regulation) - ['agree', 'agree', 'disagree', 'disagree', 'disagree'], - ['agree', 'agree', 'disagree', 'disagree', 'disagree'], - ['agree', 'agree', 'disagree', 'disagree', 'disagree'] -] + votes = [ + # Group 1: Environmental purists (anti-nuclear) + ['agree', 'disagree', 'agree', 'agree', 'agree'], + ['agree', 'disagree', 'agree', 'agree', 'agree'], + ['agree', 'disagree', 'agree', 'agree', 'agree'], + + # Group 2: Tech-focused environmentalists (pro-nuclear) + ['agree', 'agree', 'agree', 'disagree', 'agree'], + ['agree', 'agree', 'agree', 'disagree', 'agree'], + ['agree', 'agree', 'agree', 'disagree', 'agree'], + + # Group 3: Business-oriented (anti-regulation) + ['agree', 'agree', 'disagree', 'disagree', 'disagree'], + ['agree', 'agree', 'disagree', 'disagree', 'disagree'], + ['agree', 'agree', 'disagree', 'disagree', 'disagree'] + ] -clusterer = PolisClusterer() -points, clusters = clusterer.analyze_opinions(votes, statements) + clusterer = PolisClusterer() + points, clusters = clusterer.analyze_opinions(votes, statements) diff --git a/polislite_library_usage.ipynb b/polislite_library_usage.ipynb new file mode 100644 index 0000000..fed38cb --- /dev/null +++ b/polislite_library_usage.ipynb @@ -0,0 +1,156 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "authorship_tag": "ABX9TyPLsyDH4WY7BEoL4VGciHd3", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "code", + "source": [ + "!pip install git+https://github.com/patcon/polislite.git@python-package" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "9NebPULW1JTR", + "outputId": "0b95059b-d3b9-4840-8da3-8702b20318d9" + }, + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Collecting git+https://github.com/patcon/polislite.git@python-package\n", + " Cloning https://github.com/patcon/polislite.git (to revision python-package) to /tmp/pip-req-build-b8924s2s\n", + " Running command git clone --filter=blob:none --quiet https://github.com/patcon/polislite.git /tmp/pip-req-build-b8924s2s\n", + " Running command git checkout -b python-package --track origin/python-package\n", + " Switched to a new branch 'python-package'\n", + " Branch 'python-package' set up to track remote branch 'python-package' from 'origin'.\n", + " Resolved https://github.com/patcon/polislite.git to commit 9252f24c4acb6ea38a25c1b51b4546d087e91747\n", + " Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n", + " Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n", + " Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", + "Requirement already satisfied: scikit-learn>=1.6.0 in /usr/local/lib/python3.10/dist-packages (from polislite==0.1.0) (1.6.0)\n", + "Requirement already satisfied: numpy>=1.19.5 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=1.6.0->polislite==0.1.0) (1.26.4)\n", + "Requirement already satisfied: scipy>=1.6.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=1.6.0->polislite==0.1.0) (1.13.1)\n", + "Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=1.6.0->polislite==0.1.0) (1.4.2)\n", + "Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=1.6.0->polislite==0.1.0) (3.5.0)\n", + "Building wheels for collected packages: polislite\n", + " Building wheel for polislite (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", + " Created wheel for polislite: filename=polislite-0.1.0-py3-none-any.whl size=4394 sha256=051e48ee7fc35772739a0611e047949170bec18ff9792890650db6a3faeaef37\n", + " Stored in directory: /tmp/pip-ephem-wheel-cache-3q6whqzs/wheels/0d/d5/a2/fe574e20315f0bdfc7f1d4b81bbf2b2caa4c7a4e2a9ef0511f\n", + "Successfully built polislite\n", + "Installing collected packages: polislite\n", + "Successfully installed polislite-0.1.0\n" + ] + } + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "orSeaE2M1Il7", + "outputId": "ebcf7bef-1486-4379-830f-0e6d6eb3515a" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Consensus Statements:\n", + "- Climate change requires immediate action (strong agreement)\n", + "\n", + "Divisive Statements:\n", + "- Nuclear power is necessary for clean energy\n", + "- Carbon tax should be implemented globally\n", + "- Individual actions matter for sustainability\n", + "- Companies should be held liable for emissions\n", + "\n", + "Group Positions:\n", + "\n", + "Group 1 characteristics:\n", + "- strongly agrees with: Climate change requires immediate action\n", + "- strongly agrees with: Nuclear power is necessary for clean energy\n", + "- strongly disagrees with: Carbon tax should be implemented globally\n", + "- strongly disagrees with: Individual actions matter for sustainability\n", + "- strongly disagrees with: Companies should be held liable for emissions\n", + "\n", + "Group 2 characteristics:\n", + "- strongly agrees with: Climate change requires immediate action\n", + "- strongly agrees with: Nuclear power is necessary for clean energy\n", + "- strongly agrees with: Carbon tax should be implemented globally\n", + "- strongly disagrees with: Individual actions matter for sustainability\n", + "- strongly agrees with: Companies should be held liable for emissions\n", + "\n", + "Group 3 characteristics:\n", + "- strongly agrees with: Climate change requires immediate action\n", + "- strongly disagrees with: Nuclear power is necessary for clean energy\n", + "- strongly agrees with: Carbon tax should be implemented globally\n", + "- strongly agrees with: Individual actions matter for sustainability\n", + "- strongly agrees with: Companies should be held liable for emissions\n" + ] + } + ], + "source": [ + "from polislite.polislite import PolisClusterer\n", + "\n", + "# Example usage\n", + "statements = [\n", + " 'Climate change requires immediate action',\n", + " 'Nuclear power is necessary for clean energy',\n", + " 'Carbon tax should be implemented globally',\n", + " 'Individual actions matter for sustainability',\n", + " 'Companies should be held liable for emissions'\n", + "]\n", + "\n", + "votes = [\n", + " # Group 1: Environmental purists (anti-nuclear)\n", + " ['agree', 'disagree', 'agree', 'agree', 'agree'],\n", + " ['agree', 'disagree', 'agree', 'agree', 'agree'],\n", + " ['agree', 'disagree', 'agree', 'agree', 'agree'],\n", + "\n", + " # Group 2: Tech-focused environmentalists (pro-nuclear)\n", + " ['agree', 'agree', 'agree', 'disagree', 'agree'],\n", + " ['agree', 'agree', 'agree', 'disagree', 'agree'],\n", + " ['agree', 'agree', 'agree', 'disagree', 'agree'],\n", + "\n", + " # Group 3: Business-oriented (anti-regulation)\n", + " ['agree', 'agree', 'disagree', 'disagree', 'disagree'],\n", + " ['agree', 'agree', 'disagree', 'disagree', 'disagree'],\n", + " ['agree', 'agree', 'disagree', 'disagree', 'disagree']\n", + "]\n", + "\n", + "clusterer = PolisClusterer()\n", + "points, clusters = clusterer.analyze_opinions(votes, statements)" + ] + } + ] +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ca3a401 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "polislite" +version = "0.1.0" +description = "A lightweight Polis-like" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "scikit-learn>=1.6.0", +]