Create repair_table_data.py
Browse files- repair_table_data.py +213 -0
repair_table_data.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# repair_table_data.py
|
| 2 |
+
# Full Iowa State "Accumulated Repair Costs as a Percentage of New List Price" (Table 3)
|
| 3 |
+
# Source figure: American Society of Agricultural Engineers (1996).
|
| 4 |
+
# Provides: REPAIR_TABLE, get_repair_fraction(machine, hours), list_machines()
|
| 5 |
+
|
| 6 |
+
from typing import Dict, Tuple, Optional, List
|
| 7 |
+
import re
|
| 8 |
+
|
| 9 |
+
# ----------------------------
|
| 10 |
+
# 1) Canonical table (fractions, not percents)
|
| 11 |
+
# ----------------------------
|
| 12 |
+
REPAIR_TABLE: Dict[str, Dict[int, float]] = {
|
| 13 |
+
# --- Tractors ---
|
| 14 |
+
"Two-wheel drive tractor": {
|
| 15 |
+
1000: 0.01, 2000: 0.03, 3000: 0.06, 4000: 0.11, 5000: 0.18,
|
| 16 |
+
6000: 0.25, 7000: 0.34, 8000: 0.45, 9000: 0.57, 10000: 0.70
|
| 17 |
+
},
|
| 18 |
+
"Four-wheel drive tractor": {
|
| 19 |
+
1000: 0.00, 2000: 0.01, 3000: 0.03, 4000: 0.05, 5000: 0.08,
|
| 20 |
+
6000: 0.11, 7000: 0.15, 8000: 0.19, 9000: 0.24, 10000: 0.30
|
| 21 |
+
},
|
| 22 |
+
|
| 23 |
+
# --- Tillage Implements ---
|
| 24 |
+
"Moldboard plow": {200:0.02, 400:0.06, 600:0.12, 800:0.19, 1000:0.29,
|
| 25 |
+
1200:0.40, 1400:0.53, 1600:0.68, 1800:0.84, 2000:1.01},
|
| 26 |
+
"Heavy-duty disk": {200:0.01, 400:0.04, 600:0.08, 800:0.12, 1000:0.18,
|
| 27 |
+
1200:0.25, 1400:0.32, 1600:0.40, 1800:0.49, 2000:0.58},
|
| 28 |
+
"Tandem disk": {200:0.01, 400:0.04, 600:0.08, 800:0.12, 1000:0.18,
|
| 29 |
+
1200:0.25, 1400:0.32, 1600:0.40, 1800:0.49, 2000:0.58},
|
| 30 |
+
"Chisel plow": {200:0.03, 400:0.08, 600:0.14, 800:0.20, 1000:0.28,
|
| 31 |
+
1200:0.36, 1400:0.45, 1600:0.54, 1800:0.64, 2000:0.74},
|
| 32 |
+
"Field cultivator": {200:0.03, 400:0.07, 600:0.13, 800:0.20, 1000:0.27,
|
| 33 |
+
1200:0.35, 1400:0.43, 1600:0.52, 1800:0.61, 2000:0.71},
|
| 34 |
+
"Harrow": {200:0.02, 400:0.05, 600:0.08, 800:0.12, 1000:0.16,
|
| 35 |
+
1200:0.22, 1400:0.29, 1600:0.37, 1800:0.46, 2000:0.56},
|
| 36 |
+
"Roller-packer, mulcher": {200:0.02, 400:0.05, 600:0.08, 800:0.12, 1000:0.16,
|
| 37 |
+
1200:0.22, 1400:0.29, 1600:0.37, 1800:0.46, 2000:0.56},
|
| 38 |
+
"Rotary hoe": {200:0.02, 400:0.06, 600:0.11, 800:0.17, 1000:0.23,
|
| 39 |
+
1200:0.29, 1400:0.37, 1600:0.44, 1800:0.52, 2000:0.60},
|
| 40 |
+
"Row crop cultivator": {200:0.00, 400:0.02, 600:0.06, 800:0.10, 1000:0.17,
|
| 41 |
+
1200:0.25, 1400:0.36, 1600:0.48, 1800:0.62, 2000:0.78},
|
| 42 |
+
|
| 43 |
+
# --- Harvesters & Hay ---
|
| 44 |
+
"Corn picker": {200:0.00, 400:0.02, 600:0.04, 800:0.08, 1000:0.14,
|
| 45 |
+
1200:0.21, 1400:0.30, 1600:0.41, 1800:0.54, 2000:0.69},
|
| 46 |
+
"Combine (pull)": {200:0.00, 400:0.02, 600:0.04, 800:0.08, 1000:0.14,
|
| 47 |
+
1200:0.21, 1400:0.30, 1600:0.41, 1800:0.54, 2000:0.69},
|
| 48 |
+
"Potato harvester": {200:0.02, 400:0.05, 600:0.09, 800:0.14, 1000:0.19,
|
| 49 |
+
1200:0.25, 1400:0.32, 1600:0.39, 1800:0.46, 2000:0.54},
|
| 50 |
+
"Mower-conditioner": {200:0.01, 400:0.04, 600:0.08, 800:0.13, 1000:0.18,
|
| 51 |
+
1200:0.24, 1400:0.31, 1600:0.38, 1800:0.46, 2000:0.55},
|
| 52 |
+
"Mower-conditioner (rotary)": {200:0.01, 400:0.04, 600:0.08, 800:0.13, 1000:0.18,
|
| 53 |
+
1200:0.24, 1400:0.31, 1600:0.38, 1800:0.46, 2000:0.55},
|
| 54 |
+
"Rake": {200:0.02, 400:0.05, 600:0.09, 800:0.12, 1000:0.16,
|
| 55 |
+
1200:0.22, 1400:0.27, 1600:0.33, 1800:0.39, 2000:0.45},
|
| 56 |
+
"Rectangular baler": {200:0.01, 400:0.03, 600:0.06, 800:0.10, 1000:0.15,
|
| 57 |
+
1200:0.20, 1400:0.26, 1600:0.32, 1800:0.38, 2000:0.45},
|
| 58 |
+
"Large square baler": {200:0.01, 400:0.03, 600:0.06, 800:0.10, 1000:0.15,
|
| 59 |
+
1200:0.20, 1400:0.26, 1600:0.32, 1800:0.38, 2000:0.45},
|
| 60 |
+
"Forage harvester (pull)": {200:0.01, 400:0.03, 600:0.07, 800:0.10, 1000:0.15,
|
| 61 |
+
1200:0.20, 1400:0.26, 1600:0.32, 1800:0.38, 2000:0.45},
|
| 62 |
+
|
| 63 |
+
"Forage harvester (SP)": {300:0.00, 600:0.01, 900:0.02, 1200:0.04, 1500:0.07,
|
| 64 |
+
1800:0.10, 2100:0.13, 2400:0.17, 2700:0.22, 3000:0.27},
|
| 65 |
+
"Combine (SP)": {300:0.00, 600:0.01, 900:0.02, 1200:0.04, 1500:0.07,
|
| 66 |
+
1800:0.10, 2100:0.13, 2400:0.17, 2700:0.22, 3000:0.27},
|
| 67 |
+
"Windrower (SP)": {300:0.01, 600:0.02, 900:0.04, 1200:0.09, 1500:0.15,
|
| 68 |
+
1800:0.23, 2100:0.32, 2400:0.42, 2700:0.53, 3000:0.66},
|
| 69 |
+
"Cotton picker (SP)": {300:0.01, 600:0.04, 900:0.09, 1200:0.15, 1500:0.23,
|
| 70 |
+
1800:0.32, 2100:0.42, 2400:0.53, 2700:0.66, 3000:0.79},
|
| 71 |
+
|
| 72 |
+
# --- Other Implements / Misc ---
|
| 73 |
+
"Mower (sickle)": {100:0.01, 200:0.03, 300:0.06, 400:0.10, 500:0.14,
|
| 74 |
+
600:0.19, 700:0.25, 800:0.31, 900:0.38, 1000:0.46},
|
| 75 |
+
"Mower (rotary)": {100:0.00, 200:0.02, 300:0.04, 400:0.07, 500:0.11,
|
| 76 |
+
600:0.16, 700:0.22, 800:0.28, 900:0.36, 1000:0.44},
|
| 77 |
+
"Large round baler": {100:0.01, 200:0.03, 300:0.06, 400:0.10, 500:0.15,
|
| 78 |
+
600:0.20, 700:0.26, 800:0.33, 900:0.40, 1000:0.48},
|
| 79 |
+
"Sugar beet harvester": {100:0.03, 200:0.07, 300:0.12, 400:0.18, 500:0.24,
|
| 80 |
+
600:0.30, 700:0.37, 800:0.44, 900:0.51, 1000:0.59},
|
| 81 |
+
"Rotary tiller": {100:0.01, 200:0.03, 300:0.06, 400:0.09, 500:0.13,
|
| 82 |
+
600:0.17, 700:0.22, 800:0.27, 900:0.33, 1000:0.40},
|
| 83 |
+
"Row crop planter": {100:0.00, 200:0.01, 300:0.03, 400:0.05, 500:0.07,
|
| 84 |
+
600:0.09, 700:0.11, 800:0.15, 900:0.20, 1000:0.26},
|
| 85 |
+
"Grain drill": {100:0.01, 200:0.03, 300:0.06, 400:0.09, 500:0.13,
|
| 86 |
+
600:0.19, 700:0.26, 800:0.32, 900:0.40, 1000:0.47},
|
| 87 |
+
"Fertilizer spreader": {100:0.03, 200:0.08, 300:0.13, 400:0.19, 500:0.26,
|
| 88 |
+
600:0.32, 700:0.40, 800:0.47, 900:0.55, 1000:0.63},
|
| 89 |
+
|
| 90 |
+
# --- Sprayers & Wagons ---
|
| 91 |
+
"Boom-type sprayer": {200:0.05, 400:0.12, 600:0.21, 800:0.31, 1000:0.41,
|
| 92 |
+
1200:0.52, 1400:0.63, 1600:0.76, 1800:0.88, 2000:1.01},
|
| 93 |
+
"Air-carrier sprayer": {200:0.02, 400:0.05, 600:0.09, 800:0.14, 1000:0.20,
|
| 94 |
+
1200:0.27, 1400:0.34, 1600:0.42, 1800:0.51, 2000:0.61},
|
| 95 |
+
"Bean puller-windrower": {200:0.02, 400:0.05, 600:0.09, 800:0.14, 1000:0.20,
|
| 96 |
+
1200:0.27, 1400:0.34, 1600:0.42, 1800:0.51, 2000:0.61},
|
| 97 |
+
"Stalk chopper": {200:0.03, 400:0.08, 600:0.14, 800:0.20, 1000:0.28,
|
| 98 |
+
1200:0.36, 1400:0.45, 1600:0.54, 1800:0.64, 2000:0.74},
|
| 99 |
+
"Forage blower": {200:0.01, 400:0.04, 600:0.09, 800:0.15, 1000:0.22,
|
| 100 |
+
1200:0.29, 1400:0.37, 1600:0.46, 1800:0.56, 2000:0.67},
|
| 101 |
+
"Wagon": {200:0.01, 400:0.04, 600:0.07, 800:0.11, 1000:0.16,
|
| 102 |
+
1200:0.21, 1400:0.27, 1600:0.34, 1800:0.41, 2000:0.49},
|
| 103 |
+
"Forage wagon": {200:0.02, 400:0.06, 600:0.10, 800:0.14, 1000:0.19,
|
| 104 |
+
1200:0.24, 1400:0.29, 1600:0.35, 1800:0.41, 2000:0.47},
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
# ----------------------------
|
| 108 |
+
# 2) Name normalization / synonyms
|
| 109 |
+
# ----------------------------
|
| 110 |
+
_CANON = {k.lower(): k for k in REPAIR_TABLE.keys()}
|
| 111 |
+
|
| 112 |
+
_SYNONYMS = {
|
| 113 |
+
# tractors
|
| 114 |
+
"2wd": "Two-wheel drive tractor",
|
| 115 |
+
"two wheel drive": "Two-wheel drive tractor",
|
| 116 |
+
"two-wheel drive": "Two-wheel drive tractor",
|
| 117 |
+
"two wheel drive tractor": "Two-wheel drive tractor",
|
| 118 |
+
|
| 119 |
+
"4wd": "Four-wheel drive tractor",
|
| 120 |
+
"four wheel drive": "Four-wheel drive tractor",
|
| 121 |
+
"four-wheel drive": "Four-wheel drive tractor",
|
| 122 |
+
"four wheel drive tractor": "Four-wheel drive tractor",
|
| 123 |
+
|
| 124 |
+
# common short names
|
| 125 |
+
"tractor": "Two-wheel drive tractor", # default if just "tractor"
|
| 126 |
+
"sp forage harvester": "Forage harvester (SP)",
|
| 127 |
+
"sp combine": "Combine (SP)",
|
| 128 |
+
"sp windrower": "Windrower (SP)",
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def _normalize_name(s: str) -> Optional[str]:
|
| 132 |
+
key = re.sub(r"\s+", " ", s.strip().lower())
|
| 133 |
+
if key in _CANON:
|
| 134 |
+
return _CANON[key]
|
| 135 |
+
if key in _SYNONYMS:
|
| 136 |
+
return _SYNONYMS[key]
|
| 137 |
+
# try loose contains
|
| 138 |
+
for alias, canon in _SYNONYMS.items():
|
| 139 |
+
if alias in key:
|
| 140 |
+
return canon
|
| 141 |
+
# last resort: pick a canonical that fully appears
|
| 142 |
+
for canon_lower, canon in _CANON.items():
|
| 143 |
+
if canon_lower in key:
|
| 144 |
+
return canon
|
| 145 |
+
return None
|
| 146 |
+
|
| 147 |
+
# ----------------------------
|
| 148 |
+
# 3) Interpolation / extrapolation
|
| 149 |
+
# ----------------------------
|
| 150 |
+
def _sorted_points(machine: str) -> List[Tuple[int, float]]:
|
| 151 |
+
pts = list(REPAIR_TABLE[machine].items())
|
| 152 |
+
pts.sort(key=lambda x: x[0])
|
| 153 |
+
return pts
|
| 154 |
+
|
| 155 |
+
def _interp(x0: float, y0: float, x1: float, y1: float, x: float) -> float:
|
| 156 |
+
if x1 == x0:
|
| 157 |
+
return y0
|
| 158 |
+
return y0 + (y1 - y0) * ((x - x0) / (x1 - x0))
|
| 159 |
+
|
| 160 |
+
def get_repair_fraction(machine_name: str, hours: float) -> Tuple[float, str, Tuple[int, float], Tuple[int, float]]:
|
| 161 |
+
"""
|
| 162 |
+
Returns:
|
| 163 |
+
fraction (0..1+), canonical_machine_name, (x_lo,y_lo), (x_hi,y_hi)
|
| 164 |
+
Uses linear interpolation between nearest table points. Extrapolates gently
|
| 165 |
+
beyond edges (linear using last two points).
|
| 166 |
+
"""
|
| 167 |
+
if hours < 0:
|
| 168 |
+
hours = 0.0
|
| 169 |
+
|
| 170 |
+
canon = _normalize_name(machine_name) or "Two-wheel drive tractor"
|
| 171 |
+
pts = _sorted_points(canon)
|
| 172 |
+
xs = [p[0] for p in pts]
|
| 173 |
+
|
| 174 |
+
# below range
|
| 175 |
+
if hours <= xs[0]:
|
| 176 |
+
x0, y0 = pts[0]
|
| 177 |
+
x1, y1 = pts[1]
|
| 178 |
+
y = _interp(x0, y0, x1, y1, hours)
|
| 179 |
+
return max(0.0, y), canon, (x0, y0), (x1, y1)
|
| 180 |
+
|
| 181 |
+
# above range
|
| 182 |
+
if hours >= xs[-1]:
|
| 183 |
+
x0, y0 = pts[-2]
|
| 184 |
+
x1, y1 = pts[-1]
|
| 185 |
+
y = _interp(x0, y0, x1, y1, hours)
|
| 186 |
+
return max(0.0, y), canon, (x0, y0), (x1, y1)
|
| 187 |
+
|
| 188 |
+
# interior: find bracketing points
|
| 189 |
+
for i in range(1, len(pts)):
|
| 190 |
+
x0, y0 = pts[i-1]
|
| 191 |
+
x1, y1 = pts[i]
|
| 192 |
+
if x0 <= hours <= x1:
|
| 193 |
+
y = _interp(x0, y0, x1, y1, hours)
|
| 194 |
+
return max(0.0, y), canon, (x0, y0), (x1, y1)
|
| 195 |
+
|
| 196 |
+
# should not reach here
|
| 197 |
+
x0, y0 = pts[0]
|
| 198 |
+
x1, y1 = pts[1]
|
| 199 |
+
return y0, canon, (x0, y0), (x1, y1)
|
| 200 |
+
|
| 201 |
+
# ----------------------------
|
| 202 |
+
# 4) Utilities
|
| 203 |
+
# ----------------------------
|
| 204 |
+
def list_machines() -> List[str]:
|
| 205 |
+
"""Return the list of canonical machine names available in the table."""
|
| 206 |
+
return sorted(REPAIR_TABLE.keys())
|
| 207 |
+
|
| 208 |
+
def describe_machine_match(name: str) -> str:
|
| 209 |
+
"""Debug helper: shows which canonical name a user string maps to."""
|
| 210 |
+
c = _normalize_name(name)
|
| 211 |
+
if not c:
|
| 212 |
+
return f"No match for '{name}'. Known: {', '.join(list_machines()[:8])}…"
|
| 213 |
+
return f"'{name}' → '{c}'"
|