Used Car Market Analytics: Depreciation Tracking with Proxy Data

Used Car Market Analytics: Depreciation Tracking with Proxy Data

Vehicle depreciation is one of the largest costs of car ownership, yet it remains one of the least understood. In Southeast Asia, where government policies, import duties, and unique market structures create depreciation patterns unlike anywhere else in the world, data-driven depreciation analysis provides immense value to dealers, financial institutions, fleet operators, and consumers.

This guide shows how to build depreciation tracking models using data collected through proxy infrastructure from used car marketplaces across the region.

Why Depreciation Tracking Matters

For Dealers

Understanding depreciation helps dealers price trade-ins accurately, predict how long inventory will hold value, and identify vehicles with unusually strong or weak residual values.

For Financial Institutions

Banks and finance companies use depreciation data to set appropriate loan-to-value ratios, assess collateral values for outstanding vehicle loans, and price residual value guarantees.

For Fleet Operators

Fleet replacement timing depends critically on depreciation curves. Disposing of vehicles at the optimal point on their depreciation curve can save thousands per vehicle.

For Insurance Companies

Accurate depreciation data supports agreed value policies, total loss assessments, and premium calculation for diminishing value coverage.

For Consumers

Understanding how different vehicles depreciate helps consumers make purchasing decisions that minimize total cost of ownership.

Unique Depreciation Factors in Southeast Asia

Singapore: The COE Effect

Singapore’s Certificate of Entitlement system creates a depreciation pattern unlike any other market:

  • COE has a 10-year validity period, creating a hard deadline for vehicle value
  • Vehicles depreciate roughly linearly over the COE period
  • COE renewal (PARF rebate) creates a value floor at the 10-year mark
  • COE premiums fluctuate significantly, affecting both new and used car values
  • A car’s depreciation is heavily influenced by when the COE was purchased

Malaysia: AP and Tax Structure

  • Approved Permit (AP) for imported vehicles adds significant cost
  • National car brands (Proton, Perodua) depreciate differently from imports
  • Excise duties create large gaps between new and used prices
  • Right-hand drive specification limits export market options

Thailand: Local Production Advantage

  • Locally manufactured vehicles (Toyota, Honda, Isuzu, Mitsubishi) depreciate slower
  • Pickup trucks hold value exceptionally well
  • First-car buyer tax incentive has market-wide effects
  • Eco-car segment has its own depreciation dynamics

Indonesia: Volume Market Dynamics

  • Extremely high volume for certain models (Avanza, Xenia) creates deep used market
  • LCGC (Low Cost Green Car) segment with unique depreciation
  • Regional price variations are significant
  • Two-wheeler depreciation follows different patterns than cars

Building Your Depreciation Data Collection System

Data Sources

For comprehensive depreciation tracking, collect pricing data by vehicle age from these sources:

class DepreciationDataCollector:
    def __init__(self, proxy_manager):
        self.proxy_manager = proxy_manager
        self.sources = {
            "SG": ["sgcarmart", "carousell_sg", "carro_sg"],
            "MY": ["mudah", "carlist", "carsome_my"],
            "TH": ["kaidee", "one2car", "carro_th"],
            "ID": ["olx_id", "carmudi_id", "carro_id"],
        }

    def collect_depreciation_data(self, make, model, country):
        """Collect pricing data for a vehicle across multiple model years"""
        current_year = datetime.now().year
        year_data = {}

        for year in range(current_year, current_year - 15, -1):
            listings = self.collect_listings_for_year(make, model, year, country)
            if listings:
                prices = [l["price"] for l in listings if l.get("price")]
                if prices:
                    year_data[year] = {
                        "avg_price": statistics.mean(prices),
                        "median_price": statistics.median(prices),
                        "min_price": min(prices),
                        "max_price": max(prices),
                        "sample_size": len(prices),
                        "listings": listings,
                    }

        return year_data

    def collect_listings_for_year(self, make, model, year, country):
        all_listings = []
        source_names = self.sources.get(country, [])

        for source_name in source_names:
            proxy = self.proxy_manager.get_proxy(country)
            scraper = self.get_scraper(source_name, proxy)

            listings = scraper.search(
                make=make,
                model=model,
                year_from=year,
                year_to=year
            )

            for listing in listings:
                listing["source"] = source_name
                listing["country"] = country

            all_listings.extend(listings)
            time.sleep(random.uniform(1, 3))

        return all_listings

Proxy Configuration

DataResearchTools mobile proxies ensure reliable access to all pricing sources:

class DepreciationProxyManager:
    def __init__(self, api_key):
        self.api_key = api_key
        self.endpoint = "proxy.dataresearchtools.com"

    def get_proxy(self, country):
        session_id = uuid4().hex[:8]
        auth = f"{self.api_key}:country-{country}-type-mobile-session-{session_id}"
        return {
            "http": f"http://{auth}@{self.endpoint}:8080",
            "https": f"http://{auth}@{self.endpoint}:8080"
        }

Building Depreciation Models

Basic Depreciation Curve

class DepreciationModel:
    def build_curve(self, make, model, country, year_data, new_price=None):
        """Build a depreciation curve from collected data"""
        current_year = datetime.now().year

        if not new_price and current_year in year_data:
            new_price = year_data[current_year].get("median_price")

        if not new_price:
            return None

        curve = []
        for year, data in sorted(year_data.items(), reverse=True):
            age = current_year - year
            median_price = data["median_price"]
            retention_pct = (median_price / new_price) * 100

            curve.append({
                "year": year,
                "age": age,
                "median_price": median_price,
                "retention_pct": round(retention_pct, 1),
                "annual_depreciation_pct": None,  # Calculated below
                "sample_size": data["sample_size"],
            })

        # Calculate year-over-year depreciation
        for i in range(len(curve) - 1):
            current = curve[i]["median_price"]
            next_year = curve[i + 1]["median_price"]
            annual_dep = ((current - next_year) / current) * 100
            curve[i]["annual_depreciation_pct"] = round(annual_dep, 1)

        return {
            "make": make,
            "model": model,
            "country": country,
            "new_price": new_price,
            "curve": curve,
            "total_depreciation_5yr": self.calculate_n_year_depreciation(curve, 5),
            "steepest_year": self.find_steepest_year(curve),
            "value_cliff": self.detect_value_cliff(curve),
        }

    def calculate_n_year_depreciation(self, curve, n):
        """Calculate total depreciation over n years"""
        data_points = {p["age"]: p for p in curve}
        if 0 in data_points and n in data_points:
            return round(100 - data_points[n]["retention_pct"], 1)
        return None

    def find_steepest_year(self, curve):
        """Find the year with the highest depreciation rate"""
        steepest = max(
            [p for p in curve if p["annual_depreciation_pct"]],
            key=lambda x: x["annual_depreciation_pct"],
            default=None
        )
        return steepest

    def detect_value_cliff(self, curve):
        """Detect sudden drops in value (common with COE expiry in Singapore)"""
        cliffs = []
        for i in range(len(curve) - 1):
            if curve[i]["annual_depreciation_pct"] and curve[i]["annual_depreciation_pct"] > 20:
                cliffs.append({
                    "age": curve[i]["age"],
                    "drop_pct": curve[i]["annual_depreciation_pct"],
                    "reason": self.infer_cliff_reason(curve[i]["age"], curve[i].get("country")),
                })
        return cliffs

Singapore COE-Adjusted Depreciation

class SGDepreciationModel(DepreciationModel):
    def build_coe_adjusted_curve(self, make, model, year_data, coe_data):
        """Build depreciation curve accounting for COE value"""
        current_year = datetime.now().year
        curve = []

        for year, data in sorted(year_data.items(), reverse=True):
            age = current_year - year
            remaining_coe_years = max(0, 10 - age)

            # Estimate COE value component
            coe_at_registration = coe_data.get(year, {}).get("premium", 0)
            parf_value = self.calculate_parf_rebate(coe_at_registration, remaining_coe_years)

            # Vehicle value excluding COE
            body_value = data["median_price"] - parf_value

            curve.append({
                "year": year,
                "age": age,
                "total_price": data["median_price"],
                "body_value": body_value,
                "coe_value": parf_value,
                "remaining_coe_years": remaining_coe_years,
                "sample_size": data["sample_size"],
            })

        return {
            "make": make,
            "model": model,
            "country": "SG",
            "curve": curve,
            "body_depreciation_5yr": self.calculate_body_depreciation(curve, 5),
            "optimal_disposal_age": self.find_optimal_sg_disposal(curve),
        }

    def calculate_parf_rebate(self, coe_premium, remaining_years):
        """Calculate PARF rebate based on remaining COE validity"""
        if remaining_years <= 0:
            return 0

        # PARF rebate is proportional to remaining validity
        rebate_percentage = remaining_years / 10
        return coe_premium * rebate_percentage

    def find_optimal_sg_disposal(self, curve):
        """Find optimal disposal age considering COE expiry"""
        # In Singapore, the optimal disposal is typically at year 9
        # to capture remaining PARF value before 10-year expiry
        for point in curve:
            if point["age"] == 9 and point["remaining_coe_years"] == 1:
                return {
                    "recommended_age": 9,
                    "reason": "Sell before COE expiry to retain PARF rebate value",
                    "estimated_parf": point["coe_value"],
                }

        return None

Comparative Depreciation Analysis

class ComparativeDepreciation:
    def compare_models(self, models_data):
        """Compare depreciation across different vehicle models"""
        comparison = []

        for model_key, data in models_data.items():
            curve = data.get("curve", [])
            if not curve:
                continue

            five_year = next((p for p in curve if p["age"] == 5), None)
            three_year = next((p for p in curve if p["age"] == 3), None)

            comparison.append({
                "vehicle": model_key,
                "new_price": data.get("new_price"),
                "3yr_retention": three_year["retention_pct"] if three_year else None,
                "5yr_retention": five_year["retention_pct"] if five_year else None,
                "steepest_year": data.get("steepest_year", {}).get("age"),
                "steepest_drop": data.get("steepest_year", {}).get("annual_depreciation_pct"),
            })

        return sorted(comparison, key=lambda x: x.get("5yr_retention", 0) or 0, reverse=True)

    def find_best_value_retention(self, country, segment, min_sample=10):
        """Find vehicles with best value retention in a segment"""
        all_models = self.db.get_all_models_in_segment(country, segment)
        results = []

        for make, model in all_models:
            curve_data = self.db.get_depreciation_curve(make, model, country)
            if curve_data and curve_data.get("total_depreciation_5yr") is not None:
                if curve_data.get("min_sample_size", 0) >= min_sample:
                    results.append({
                        "make": make,
                        "model": model,
                        "5yr_depreciation_pct": curve_data["total_depreciation_5yr"],
                        "5yr_retention_pct": 100 - curve_data["total_depreciation_5yr"],
                    })

        return sorted(results, key=lambda x: x["5yr_depreciation_pct"])

Time-Series Depreciation Tracking

Tracking Changes in Depreciation Patterns Over Time

class DepreciationTrendTracker:
    def __init__(self, db):
        self.db = db

    def track_depreciation_shifts(self, make, model, country, lookback_months=12):
        """Track how depreciation patterns change over time"""
        current_curve = self.db.get_latest_curve(make, model, country)
        historical_curve = self.db.get_curve_from_date(
            make, model, country,
            datetime.now() - timedelta(days=lookback_months * 30)
        )

        if not current_curve or not historical_curve:
            return None

        shifts = []
        for age in range(1, 11):
            current_retention = self.get_retention_at_age(current_curve, age)
            historical_retention = self.get_retention_at_age(historical_curve, age)

            if current_retention and historical_retention:
                shift = current_retention - historical_retention
                shifts.append({
                    "age": age,
                    "current_retention": current_retention,
                    "previous_retention": historical_retention,
                    "shift": round(shift, 1),
                    "direction": "improving" if shift > 0 else "worsening",
                })

        overall_direction = "improving" if sum(s["shift"] for s in shifts) > 0 else "worsening"

        return {
            "make": make,
            "model": model,
            "country": country,
            "period": f"Last {lookback_months} months",
            "overall_trend": overall_direction,
            "age_shifts": shifts,
        }

Predictive Depreciation

Forecasting Future Values

class DepreciationForecaster:
    def forecast_value(self, vehicle, current_value, target_age_years, country):
        """Predict future value based on historical depreciation patterns"""
        make = vehicle["make"]
        model = vehicle["model"]
        current_age = datetime.now().year - vehicle["year"]

        # Get historical depreciation data
        historical = self.db.get_depreciation_data(make, model, country)

        if not historical:
            # Fall back to segment average
            segment = self.classify_segment(make, model)
            historical = self.db.get_segment_depreciation(segment, country)

        if not historical:
            return None

        # Calculate average annual depreciation rate for target age range
        annual_rates = []
        for age in range(current_age, current_age + target_age_years):
            rate = self.get_depreciation_rate_at_age(historical, age)
            if rate:
                annual_rates.append(rate / 100)

        if not annual_rates:
            return None

        # Project future value
        projected_value = current_value
        for rate in annual_rates:
            projected_value *= (1 - rate)

        return {
            "current_value": current_value,
            "projected_value": round(projected_value, 2),
            "years_ahead": target_age_years,
            "projected_age": current_age + target_age_years,
            "total_depreciation_pct": round((1 - projected_value / current_value) * 100, 1),
            "annual_avg_depreciation_pct": round(statistics.mean([r * 100 for r in annual_rates]), 1),
            "confidence": "high" if len(annual_rates) == target_age_years else "medium",
        }

Storage and Visualization

Database Schema

CREATE TABLE depreciation_curves (
    id SERIAL PRIMARY KEY,
    make VARCHAR(100),
    model VARCHAR(200),
    variant VARCHAR(200),
    country VARCHAR(5),
    vehicle_age INTEGER,
    avg_price DECIMAL(15, 2),
    median_price DECIMAL(15, 2),
    sample_size INTEGER,
    retention_pct DECIMAL(5, 1),
    annual_depreciation_pct DECIMAL(5, 1),
    currency VARCHAR(5),
    snapshot_date DATE DEFAULT CURRENT_DATE,
    UNIQUE (make, model, country, vehicle_age, snapshot_date)
);

CREATE INDEX idx_depreciation_lookup ON depreciation_curves(make, model, country, snapshot_date);

Visualization Data Endpoints

@app.get("/api/v1/depreciation/{make}/{model}")
async def get_depreciation_curve(make: str, model: str, country: str = "SG"):
    curve_data = db.get_latest_curve(make, model, country)
    if not curve_data:
        raise HTTPException(status_code=404, detail="No depreciation data found")

    return {
        "make": make,
        "model": model,
        "country": country,
        "curve": curve_data,
        "chart_data": {
            "x_axis": [p["age"] for p in curve_data],
            "y_retention": [p["retention_pct"] for p in curve_data],
            "y_price": [p["median_price"] for p in curve_data],
        },
        "summary": {
            "best_retention_year": min(curve_data, key=lambda x: x.get("annual_depreciation_pct", 100))["age"],
            "worst_retention_year": max(curve_data, key=lambda x: x.get("annual_depreciation_pct", 0))["age"],
        }
    }

Conclusion

Depreciation tracking is one of the most valuable applications of systematic vehicle data collection. By scraping pricing data across multiple model years from Southeast Asian automotive platforms, you can build depreciation models that inform better decisions across the automotive value chain.

DataResearchTools mobile proxies provide the reliable data collection infrastructure needed to gather the historical and current pricing data that depreciation models require. With coverage across Singapore, Malaysia, Thailand, Indonesia, and the Philippines, DataResearchTools ensures you can build comprehensive depreciation datasets for every major vehicle in the region.

The insights from depreciation analysis compound over time. Each data collection cycle adds to your historical dataset, making your models more accurate and your forecasts more reliable. Start collecting today and build the depreciation intelligence that gives your business a lasting competitive edge.


Related Reading

Scroll to Top