← back to La Permits School

summary.py

122 lines

#!/usr/bin/env python3
"""
summary.py — class-ready descriptive stats from permits.csv (school project). $0, stdlib only.

Prints: headline totals, permits & $ by year, top contractors by build value,
total build $ by City Council district, permit-type mix, and the biggest single
projects — all as clean text tables you can paste into a report or slide.

Usage:
  python3 summary.py                 # reads permits.csv
  python3 summary.py --in firms.csv --top 15
"""

import argparse
import csv
from collections import defaultdict

OWNER_PLACEHOLDERS = {"OWNER-BUILDER", "OWNER BUILDER", "OWNER/BUILDER"}


def money(v):
    try:
        return float(v)
    except (TypeError, ValueError):
        return 0.0


def usd(n):
    return "$" + format(int(n), ",")


def bar(frac, width=24):
    return "█" * int(round(frac * width))


def table(title, rows, cols):
    """rows = list of tuples; cols = list of (header, width, align) where align in 'lr'."""
    print(f"\n{title}")
    print("-" * len(title))
    head = "  ".join(("{:<%d}" % w if a == "l" else "{:>%d}" % w).format(h) for h, w, a in cols)
    print(head)
    for r in rows:
        print("  ".join(("{:<%d}" % w if a == "l" else "{:>%d}" % w).format(str(c)) for c, (_, w, a) in zip(r, cols)))


def main():
    ap = argparse.ArgumentParser(description="Descriptive stats from a permits CSV.")
    ap.add_argument("--in", dest="infile", default="permits.csv")
    ap.add_argument("--top", type=int, default=10, help="rows per ranking (default 10)")
    args = ap.parse_args()

    rows = list(csv.DictReader(open(args.infile, newline="", encoding="utf-8")))
    if not rows:
        print("No rows in", args.infile)
        return

    total_val = sum(money(r.get("valuation")) for r in rows)
    dates = sorted(r["issue_date"][:10] for r in rows if r.get("issue_date"))

    by_year = defaultdict(lambda: [0, 0.0])           # year -> [count, $]
    by_cd = defaultdict(lambda: [0, 0.0])             # council district -> [count, $]
    by_type = defaultdict(lambda: [0, 0.0])           # permit_type -> [count, $]
    by_firm = defaultdict(lambda: [0, 0.0])           # contractor -> [count, $]
    biggest = []                                      # (val, address, contractor)

    for r in rows:
        v = money(r.get("valuation"))
        yr = (r.get("issue_date") or "")[:4]
        if yr:
            by_year[yr][0] += 1; by_year[yr][1] += v
        cd = (r.get("council_district") or "?").strip() or "?"
        by_cd[cd][0] += 1; by_cd[cd][1] += v
        pt = (r.get("permit_type") or "?").strip() or "?"
        by_type[pt][0] += 1; by_type[pt][1] += v
        name = (r.get("contractors_business_name") or "").strip()
        if name and name.upper() not in OWNER_PLACEHOLDERS:
            by_firm[name][0] += 1; by_firm[name][1] += v
        biggest.append((v, r.get("address", ""), name or "(none)"))

    # ---- headline ----
    print("=" * 58)
    print("LA BUILDING PERMITS — SUMMARY   (cost: $0, local)")
    print("=" * 58)
    print(f"  Permits           : {len(rows):,}")
    print(f"  Total valuation   : {usd(total_val)}")
    print(f"  Avg per permit    : {usd(total_val / len(rows))}")
    print(f"  Date range        : {dates[0]}  ->  {dates[-1]}")
    print(f"  Unique contractors: {len(by_firm):,} (excl. owner-builder)")

    # ---- by year ----
    ymax = max(c for c, _ in by_year.values())
    table("PERMITS BY YEAR", [
        (y, f"{c:,}", usd(s), bar(c / ymax)) for y, (c, s) in sorted(by_year.items())
    ], [("Year", 6, "l"), ("Permits", 9, "r"), ("Valuation", 18, "r"), ("", 24, "l")])

    # ---- top contractors ----
    top_firms = sorted(by_firm.items(), key=lambda kv: kv[1][1], reverse=True)[:args.top]
    table(f"TOP {args.top} CONTRACTORS BY BUILD VALUE", [
        (i + 1, usd(s), f"{c}", name[:38]) for i, (name, (c, s)) in enumerate(top_firms)
    ], [("#", 3, "r"), ("Total $", 18, "r"), ("Permits", 8, "r"), ("Contractor", 38, "l")])

    # ---- by council district ----
    top_cd = sorted(by_cd.items(), key=lambda kv: kv[1][1], reverse=True)[:args.top]
    table(f"TOP {args.top} COUNCIL DISTRICTS BY BUILD VALUE", [
        (cd, f"{c:,}", usd(s)) for cd, (c, s) in top_cd
    ], [("District", 9, "l"), ("Permits", 9, "r"), ("Valuation", 18, "r")])

    # ---- permit type mix ----
    table("PERMIT-TYPE MIX", [
        (pt[:20], f"{c:,}", usd(s)) for pt, (c, s) in sorted(by_type.items(), key=lambda kv: kv[1][1], reverse=True)
    ], [("Type", 20, "l"), ("Permits", 9, "r"), ("Valuation", 18, "r")])

    # ---- biggest single projects ----
    table(f"BIGGEST {args.top} SINGLE PROJECTS", [
        (usd(v), (a or "?")[:30], c[:30]) for v, a, c in sorted(biggest, reverse=True)[:args.top]
    ], [("Valuation", 16, "r"), ("Address", 30, "l"), ("Contractor", 30, "l")])
    print()


if __name__ == "__main__":
    main()