Spaces:
Sleeping
Sleeping
Upload salvage_lookup.py
Browse files- salvage_lookup.py +155 -0
salvage_lookup.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# salvage_lookup.py — robust salvage class & fraction lookup (Iowa PM 710 tables)
|
| 2 |
+
|
| 3 |
+
from typing import Dict, Optional
|
| 4 |
+
from salvage_table_data import SALVAGE_TABLE
|
| 5 |
+
|
| 6 |
+
# -------- Linear helpers --------
|
| 7 |
+
def _lin(x1, y1, x2, y2, x):
|
| 8 |
+
if x1 == x2:
|
| 9 |
+
return float(y1)
|
| 10 |
+
return float(y1) + (float(y2) - float(y1)) * ((float(x) - float(x1)) / (float(x2) - float(x1)))
|
| 11 |
+
|
| 12 |
+
def _interp_on_sorted(xs, ys, x):
|
| 13 |
+
xs = list(xs); ys = list(ys)
|
| 14 |
+
if len(xs) == 1:
|
| 15 |
+
return float(ys[0])
|
| 16 |
+
if x <= xs[0]:
|
| 17 |
+
return _lin(xs[0], ys[0], xs[1], ys[1], x)
|
| 18 |
+
if x >= xs[-1]:
|
| 19 |
+
return _lin(xs[-2], ys[-2], xs[-1], ys[-1], x)
|
| 20 |
+
for i in range(len(xs) - 1):
|
| 21 |
+
if xs[i] <= x <= xs[i+1]:
|
| 22 |
+
return _lin(xs[i], ys[i], xs[i+1], ys[i+1], x)
|
| 23 |
+
return float(ys[-1])
|
| 24 |
+
|
| 25 |
+
# -------- Table access that tolerates case/spacing --------
|
| 26 |
+
def _get_table_for(mclass: str) -> Optional[Dict]:
|
| 27 |
+
if not mclass:
|
| 28 |
+
return None
|
| 29 |
+
# exact
|
| 30 |
+
t = SALVAGE_TABLE.get(mclass)
|
| 31 |
+
if t is not None:
|
| 32 |
+
return t
|
| 33 |
+
# lower
|
| 34 |
+
t = SALVAGE_TABLE.get(mclass.lower())
|
| 35 |
+
if t is not None:
|
| 36 |
+
return t
|
| 37 |
+
# strip spaces/commas/hyphens variants
|
| 38 |
+
mnorm = "".join(ch for ch in mclass.lower() if ch.isalnum())
|
| 39 |
+
for k in SALVAGE_TABLE.keys():
|
| 40 |
+
knorm = "".join(ch for ch in k.lower() if ch.isalnum())
|
| 41 |
+
if knorm == mnorm:
|
| 42 |
+
return SALVAGE_TABLE[k]
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
# -------- Public: salvage fraction lookup --------
|
| 46 |
+
def get_salvage_fraction(mclass: Optional[str], annual_hours: float, age_years: float) -> Optional[float]:
|
| 47 |
+
"""
|
| 48 |
+
For tractors/combines (banded by annual hours): interpolate across bands, then across age.
|
| 49 |
+
For implements (age-only): ignore annual_hours and interpolate across age.
|
| 50 |
+
Returns fraction of new list price at given age.
|
| 51 |
+
"""
|
| 52 |
+
if not mclass:
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
table = _get_table_for(mclass)
|
| 56 |
+
if table is None:
|
| 57 |
+
return None
|
| 58 |
+
|
| 59 |
+
age = float(int(round(age_years))) # PM 710 ages are integer 1..20
|
| 60 |
+
|
| 61 |
+
# IMPLEMENTS: either {age: frac} or {0:{age:frac}}
|
| 62 |
+
first_val = next(iter(table.values()))
|
| 63 |
+
if isinstance(first_val, (int, float)):
|
| 64 |
+
ages = sorted(table.keys())
|
| 65 |
+
vals = [table[a] for a in ages]
|
| 66 |
+
return float(_interp_on_sorted(ages, vals, age))
|
| 67 |
+
if len(table) == 1 and isinstance(first_val, dict):
|
| 68 |
+
inner = first_val
|
| 69 |
+
ages = sorted(inner.keys())
|
| 70 |
+
vals = [inner[a] for a in ages]
|
| 71 |
+
return float(_interp_on_sorted(ages, vals, age))
|
| 72 |
+
|
| 73 |
+
# TRACTORS / COMBINES: {band_hours: {age: frac}}
|
| 74 |
+
bands = sorted(table.keys())
|
| 75 |
+
def frac_at(band):
|
| 76 |
+
col = table[band]
|
| 77 |
+
ages = sorted(col.keys())
|
| 78 |
+
vals = [col[a] for a in ages]
|
| 79 |
+
return _interp_on_sorted(ages, vals, age)
|
| 80 |
+
|
| 81 |
+
if len(bands) == 1:
|
| 82 |
+
return float(frac_at(bands[0]))
|
| 83 |
+
|
| 84 |
+
# Interpolate across hour bands at the given annual_hours
|
| 85 |
+
if annual_hours <= bands[0]:
|
| 86 |
+
f1, f2 = frac_at(bands[0]), frac_at(bands[1])
|
| 87 |
+
return float(_lin(bands[0], f1, bands[1], f2, annual_hours))
|
| 88 |
+
if annual_hours >= bands[-1]:
|
| 89 |
+
f1, f2 = frac_at(bands[-2]), frac_at(bands[-1])
|
| 90 |
+
return float(_lin(bands[-2], f1, bands[-1], f2, annual_hours))
|
| 91 |
+
for i in range(len(bands) - 1):
|
| 92 |
+
lo, hi = bands[i], bands[i+1]
|
| 93 |
+
if lo <= annual_hours <= hi:
|
| 94 |
+
f_lo, f_hi = frac_at(lo), frac_at(hi)
|
| 95 |
+
return float(_lin(lo, f_lo, hi, f_hi, annual_hours))
|
| 96 |
+
|
| 97 |
+
return None # should not hit
|
| 98 |
+
|
| 99 |
+
# -------- Classifier (one definitive version) --------
|
| 100 |
+
OVERRIDE_MAP = {
|
| 101 |
+
# Your course-specific mapping choice:
|
| 102 |
+
# Treat disks as "plows" so they use the plow salvage column (as you requested).
|
| 103 |
+
"heavy-duty disk": "plows",
|
| 104 |
+
"heavy duty disk": "plows",
|
| 105 |
+
"disk": "plows",
|
| 106 |
+
"disc": "plows",
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
def classify_machine_for_salvage(name: str, hp: Optional[float]) -> Optional[str]:
|
| 110 |
+
"""
|
| 111 |
+
Map a free-text equipment string (+ optional hp) to the PM 710 class names used in SALVAGE_TABLE.
|
| 112 |
+
"""
|
| 113 |
+
s = (name or "").lower().strip()
|
| 114 |
+
|
| 115 |
+
# 0) explicit overrides first (substring match)
|
| 116 |
+
for key, cls in OVERRIDE_MAP.items():
|
| 117 |
+
if key in s:
|
| 118 |
+
return cls
|
| 119 |
+
|
| 120 |
+
# 1) Tractors (three HP bands)
|
| 121 |
+
if "tractor" in s:
|
| 122 |
+
try:
|
| 123 |
+
h = float(hp) if hp is not None else None
|
| 124 |
+
except Exception:
|
| 125 |
+
h = None
|
| 126 |
+
if h is None:
|
| 127 |
+
return "80-149 hp tractor"
|
| 128 |
+
if h >= 150:
|
| 129 |
+
return "150+ hp tractor"
|
| 130 |
+
if h >= 80:
|
| 131 |
+
return "80-149 hp tractor"
|
| 132 |
+
return "30-79 hp tractor"
|
| 133 |
+
|
| 134 |
+
# 2) Combines / Forage harvesters
|
| 135 |
+
if any(k in s for k in ["combine", "forage harvester", "forage-harvester", "silage harvester"]):
|
| 136 |
+
return "combine/forage harvester"
|
| 137 |
+
|
| 138 |
+
# 3) Implements (Table 1b buckets)
|
| 139 |
+
if any(k in s for k in ["plow", "moldboard"]):
|
| 140 |
+
return "plows"
|
| 141 |
+
if any(k in s for k in ["disk", "disc", "harrow", "chisel", "cultivator", "roller", "mulcher", "rotary hoe", "tiller"]):
|
| 142 |
+
return "other tillage"
|
| 143 |
+
if any(k in s for k in ["planter", "drill", "sprayer", "boom sprayer"]):
|
| 144 |
+
return "planter, drill, sprayer"
|
| 145 |
+
if any(k in s for k in ["mower", "chopper", "windrower"]):
|
| 146 |
+
return "mower, chopper"
|
| 147 |
+
if "baler" in s:
|
| 148 |
+
return "baler"
|
| 149 |
+
if any(k in s for k in ["swather", "rake"]):
|
| 150 |
+
return "swather, rake"
|
| 151 |
+
if any(k in s for k in ["truck", "pickup", "vehicle"]):
|
| 152 |
+
return "vehicle"
|
| 153 |
+
|
| 154 |
+
# 4) Fallback
|
| 155 |
+
return "others"
|