feat: rewrite K-factor engine with history tracking and outlier detection

Replace simple exponential smoothing with a rolling-average K-factor
system backed by a new auto_kfactor_history table. Budget fills are
detected and excluded from calculations, outliers beyond 2-sigma are
flagged, and confidence scores track data quality per customer.
Adds backfill endpoint, auto-create for missing estimation records,
and manual house_factor PUT endpoints for both auto and regular customers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 17:54:27 -05:00
parent 764c094eed
commit c134c05947
5 changed files with 473 additions and 105 deletions

View File

@@ -2,6 +2,7 @@ import logging
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from database import session
from sqlalchemy import func
from datetime import date
@@ -27,6 +28,9 @@ TANK_MAX_FILLS = {
}
class HouseFactorUpdate(BaseModel):
house_factor: float
router = APIRouter(
prefix="/fixstuff_customer",
@@ -256,10 +260,32 @@ def estimate_customer_gallons_specific(customer_id: int):
).first()
if not customer_estimate:
return JSONResponse(content={
"error": f"No fuel estimation data found for customer {customer_id}",
"solution": "Run the populate_estimates endpoint first to initialize customer data."
})
# Auto-create record from customer data
customer = session.query(Customer_Customer).filter(
Customer_Customer.id == customer_id
).first()
if not customer:
return JSONResponse(content={"error": f"Customer {customer_id} not found"}, status_code=404)
customer_estimate = Customer_estimate_gallons(
customer_id=customer.id,
account_number=customer.account_number,
customer_town=customer.customer_town,
customer_state=customer.customer_state,
customer_address=customer.customer_address,
customer_zip=customer.customer_zip,
customer_full_name=f"{customer.customer_first_name} {customer.customer_last_name}".strip(),
estimated_gallons_left=Decimal('100'),
estimated_gallons_left_prev_day=Decimal('100'),
tank_size='275',
house_factor=Decimal('0.12'),
auto_status=1,
hot_water_summer=0
)
session.add(customer_estimate)
session.commit()
session.refresh(customer_estimate)
logger.info(f"Auto-created Customer_estimate_gallons record for customer {customer_id}")
deliveries = session.query(Delivery).filter(
Delivery.customer_id == customer_estimate.customer_id,
@@ -448,3 +474,25 @@ def populate_customer_estimates():
}
return JSONResponse(content=jsonable_encoder(result))
@router.put("/house_factor/{customer_id}", status_code=200)
def update_customer_house_factor(customer_id: int, body: HouseFactorUpdate):
logger.info(f"PUT /fixstuff_customer/house_factor/{customer_id}")
customer_estimate = session.query(Customer_estimate_gallons).filter(
Customer_estimate_gallons.customer_id == customer_id
).first()
if not customer_estimate:
return JSONResponse(content={"error": "Customer estimate record not found"}, status_code=404)
customer_estimate.house_factor = Decimal(str(round(body.house_factor, 4)))
session.commit()
session.refresh(customer_estimate)
return JSONResponse(content=jsonable_encoder({
"id": customer_estimate.id,
"customer_id": customer_estimate.customer_id,
"house_factor": float(customer_estimate.house_factor),
"message": "House factor updated"
}), status_code=200)