← 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()