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_listingsProxy 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 cliffsSingapore 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 NoneComparative 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.
- Automotive Inventory Tracking Across Multiple Dealer Websites
- Automotive Review Aggregation Using Proxy Networks
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)
- API vs Web Scraping: When You Need Proxies (and When You Don’t)
- Automotive Inventory Tracking Across Multiple Dealer Websites
- Automotive Review Aggregation Using Proxy Networks
- aiohttp + BeautifulSoup: Async Python Scraping
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)
- Automotive Inventory Tracking Across Multiple Dealer Websites
- Automotive Review Aggregation Using Proxy Networks
- aiohttp + BeautifulSoup: Async Python Scraping
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)
Related Reading
- Automotive Inventory Tracking Across Multiple Dealer Websites
- Automotive Review Aggregation Using Proxy Networks
- aiohttp + BeautifulSoup: Async Python Scraping
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)