Before you begin
- Python 3.12+
Set up your example
- Save the complete code as weighted-choice-comparison.py.
- Open a terminal in the folder containing the script.
- Create the supplied sample files at the shown relative paths.
Sample files in this download:
- choices.tsv
Download the ZIP, extract it, and open a terminal inside the extracted folder. The source file is weighted-choice-comparison.py.
Run the script
python weighted-choice-comparison.py --input "choices.tsv" --output "choice-scores.txt"If your system uses python3 instead of python, use that command for Python examples. Keep paths containing spaces in quotes. Required inputs and optional settings are described in the code and setup steps.
What to expect
Option A scores 4.333 and Option B scores 3.667 using your supplied weights; the tool does not choose criteria for you.
The complete script
import argparse, ast, collections, difflib, fnmatch, gzip, hashlib, io, json, math, os, random, re, shutil, statistics, string, sys, tarfile, textwrap, time, unicodedata, zipfile
from pathlib import Path
import xml.etree.ElementTree as ET
def write_text(path, content):
path=Path(path)
path.parent.mkdir(parents=True,exist_ok=True)
with path.open('x',encoding='utf-8',newline='') as h: h.write(content)
print('Created',path)
def fresh_dir(path):
path=Path(path); path.mkdir(parents=True,exist_ok=False); return path
def files_under(root):
root=Path(root)
if not root.is_dir(): raise ValueError('Input must be a directory')
return sorted((p for p in root.rglob('*') if p.is_file() and not p.is_symlink()),key=lambda p:p.as_posix().casefold())
def digest(path):
h=hashlib.sha256()
with Path(path).open('rb') as f:
for b in iter(lambda:f.read(1048576),b''): h.update(b)
return h.hexdigest()
def positive(n,name):
if n<=0: raise ValueError(name+' must be positive')
def destination(root,name):
name=name.replace('\\','/')
if name.startswith('/') or any(p=='..' or ':' in p for p in name.split('/')): raise ValueError('Unsafe path: '+name)
p=(root/name).resolve()
if p!=root.resolve() and root.resolve() not in p.parents: raise ValueError('Path escapes destination')
return p
def zip_members(z,limit=500000000):
seen=set(); total=0; result=[]
for i in z.infolist():
if i.is_dir(): continue
destination(Path.cwd(),i.filename)
key=i.filename.replace('\\','/').casefold()
if key in seen: raise ValueError('Duplicate/case-colliding member: '+i.filename)
seen.add(key)
if ((i.external_attr>>16)&0o170000)==0o120000: raise ValueError('Symlinks unsupported')
total+=i.file_size
if total>limit: raise ValueError('Expanded data exceeds limit')
result.append(i)
return result
def copy_member(z,i,root):
p=destination(root,i.filename); p.parent.mkdir(parents=True,exist_ok=True)
with z.open(i) as src,p.open('xb') as dst: shutil.copyfileobj(src,dst)
def read_input(a): return a.input.read_text(encoding='utf-8-sig')
NS={'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main','r':'http://schemas.openxmlformats.org/officeDocument/2006/relationships'}
def docx_root(path,member='word/document.xml'):
with zipfile.ZipFile(path) as z:
i=z.getinfo(member)
if i.file_size>30000000: raise ValueError('Document XML exceeds 30 MB')
return ET.fromstring(z.read(i))
def pdf_reader(path):
try: from pypdf import PdfReader
except ImportError as e: raise ValueError('Install dependency: python -m pip install pypdf') from e
r=PdfReader(path)
if r.is_encrypted: raise ValueError('Encrypted PDF unsupported')
return r
def pdf_writer():
try: from pypdf import PdfWriter
except ImportError as e: raise ValueError('Install dependency: python -m pip install pypdf') from e
return PdfWriter()
def save_pdf(w,p):
Path(p).parent.mkdir(parents=True,exist_ok=True)
with Path(p).open('xb') as h: w.write(h)
print('Created',p)
def md_headings(content):
fence=None
for number,line in enumerate(content.splitlines(),1):
mark=re.match(r'^\s{0,3}(`{3,}|~{3,})',line)
if mark:
char=mark.group(1)[0]
if fence is None: fence=char
elif fence==char: fence=None
continue
if fence is None:
m=re.match(r'^(#{1,6})\s+(.+?)\s*#*\s*$',line)
if m: yield number,len(m.group(1)),m.group(2)
def main():
parser=argparse.ArgumentParser(description='Score personal choices from explicit criteria, weights, and option scores.')
parser.add_argument('--output',type=Path,required=True,help='New result path; must not exist')
parser.add_argument('--input',type=Path,required=True,help='Source file or folder')
args=parser.parse_args()
import csv
rows=list(csv.reader(io.StringIO(read_input(args)),delimiter='\t'))
if len(rows)<2 or len(rows[0])<3: raise ValueError('Need criterion, weight, and at least one option')
options=rows[0][2:];totals=[0.0]*len(options);totalweight=0
for row in rows[1:]:
if len(row)!=len(rows[0]): raise ValueError('Inconsistent row width')
weight=float(row[1])
if weight<0: raise ValueError('Weights cannot be negative')
totalweight+=weight
for i,score in enumerate(row[2:]): totals[i]+=weight*float(score)
positive(totalweight,'total weight');rank=sorted(zip(options,totals),key=lambda pair:-pair[1])
write_text(args.output,'\n'.join(f'{name}\tweighted average {score/totalweight:.3f}' for name,score in rank))
if __name__=='__main__':
try: main()
except (OSError,ValueError,KeyError,zipfile.BadZipFile,tarfile.TarError,ET.ParseError) as error:
print('Error:',error,file=sys.stderr); sys.exit(2)
Open source as plain text · Download source and sample files
How it works
- Score personal choices from explicit criteria, weights, and option scores.
- Customization in this script: Change the descending rank sort key or the weighted-average display if lower scores should rank first.
- Review the output before using it elsewhere; existing output paths are refused.
Make it your own
Change the task behavior
Change the descending rank sort key or the weighted-average display if lower scores should rank first.
Choose a report path
Change --output to a new local file; existing results are refused.
Inspect available options
Run python weighted-choice-comparison.py --help to see the task parameters and change their named flags in the example command.
Change one setting at a time, run it again on sample data, and compare the output. The customization guide walks through this process.
If something goes wrong
The chosen source cannot be read.
Check the source path, quote paths with spaces, and confirm the file or folder is readable.
The output already exists.
Use a new --output path; existing results are deliberately preserved.
A task-specific argument is rejected.
Run python weighted-choice-comparison.py --help, then compare the options with the complete example command and supplied sample format.
Reference & next steps
These official references explain the underlying tools. The Scriptage example and guide are original instructional content. See verification notes for what has been checked and the limits of those checks.
Copies a template for your own notes. It does not send anything.