ailiteracynepal 🇳🇵
पाठ आकार

अध्याय ३ · खण्ड II · 32 मिनेट

काठमाडौंका घर भाडा पूर्वानुमान

पूर्ण कार्यगत उदाहरण। काठमाडौंका भाडा लिस्टिङहरूको सानो डाटासेट स्क्र्याप, सफा, र फिचर इन्जिनियर गर्नुहोस्; रेखीय रिग्रेसन तालिम दिनुहोस्; इमानदारीसँग मूल्याङ्कन गर्नुहोस्। निर्माताले हप्ता-अन्तमा सक्ने र घरधनीले वास्तवमै प्रयोग गर्न सक्ने प्रकारको परियोजना।

अब हामी वास्तवमै तैनात गर्न सकिने समस्यामा रेखीय रिग्रेसन प्रयोग गर्छौँ: लिस्टिङ फिचरबाट काठमाडौं भाडा पूर्वानुमान। डाटा सानो छ (300 लिस्टिङ पर्याप्त छन्), फिचर सहज छन्, र नतिजा एक मोडेल हो जसले — फ्ल्याटको आकार, कोठा, क्षेत्र, र केही अन्य विशेषताहरू दिइयो — भाडा लगभग रु 4,000-7,000 भित्र पूर्वानुमान गर्छ। यो खण्डको अन्त्यसम्ममा तपाईंसँग काम गर्ने रिग्रेसन पाइपलाइन हुनेछ जुन तपाईंले आफ्नै छिमेकमा कुनै पनि स्थानीय पूर्वानुमान समस्यामा अनुकूलन गर्न सक्नुहुन्छ।

डाटासेट

वास्तविक काठमाडौं भाडा डाटा केही स्रोतबाट आउँछ:

  • Hamrobazar लिस्टिङ। सार्वजनिक, स्क्र्याप गर्न मिल्ने (भद्रसँग; Course 02 अध्याय 2 खण्ड 3 सम्झनुहोस्)। कुनै पनि समयमा लगभग 300-1,000 सक्रिय भाडा लिस्टिङ।
  • OnlineKhabar real estate, Khojibetha, ImeKhotnu — यस्तै स्क्र्याप गर्न मिल्ने निर्देशिका।
  • Facebook Marketplace — स्क्र्याप गर्न धेरै गाह्रो; कोसिस नगर्नुहोस्।
  • आफ्नो छिमेक। घरधनीहरूलाई सिधै सोध्नुहोस्; भाडा, क्षेत्र, आकार, कोठासहित 50-100 लिस्टिङ सङ्कलन गर्नुहोस्।

यो खण्डका लागि हामी मान्नेछौं तपाईंसँग data/raw/kathmandu_rentals.csv मा लगभग यी स्तम्भसहितको CSV छ:

listing_id, rent_npr, size_sqft, bedrooms, bathrooms, area, parking,
furnished, floor, building_type, date_posted, source_url

सानो तर वास्तविक चेतावनी: भाडा लिस्टिङमा माग्ने मूल्य हुन्छन्, सहमत मूल्य होइनन्। भाडावालले तिर्ने वास्तविक भाडा प्रायः 5-15% कम हुन्छ। मोडेलले लिस्टिङ मूल्य पूर्वानुमान गर्नेछ; त्यो सावधानी स्पष्ट रूपमा सञ्चार गर्नुहोस्।

लोड र पहिलो हेराइ

import pandas as pd

df = pd.read_csv("data/raw/kathmandu_rentals.csv")
print(df.shape)
print(df.dtypes)
print()
print(df["rent_npr"].describe())
print()
print(df["area"].value_counts().head(10))

तपाईंले लगभग 300 पङ्क्ति, 11 स्तम्भ, औसत भाडा रु 35,000–45,000 वरिपरि, र उच्च भाडातर्फ लामो पुच्छर देख्नुपर्छ। area स्तम्भले थमेल, बालुवाटार, सानेपा, ललितपुर, पाटन, आदि देखाउनेछ — “सबैभन्दा चाहिने” क्षेत्रहरूमा सबैभन्दा बढी लिस्टिङ हुन्छन्।

चरण 1 — सफा गर्ने

Course 02 अध्याय 3 का सबै कुरा प्रयोग गर्नुहोस्:

import re

# क्षेत्र नामहरू सामान्यीकरण
df["area"] = df["area"].str.strip().str.title()
area_aliases = {
    "Kupondol": "Kupondole",
    "Lalitpur Sub-Metro": "Lalitpur",
    "Patan Dhoka": "Patan",
}
df["area"] = df["area"].replace(area_aliases)

# आवश्यकहरू छुटेका पङ्क्ति हटाउनुहोस्
df = df.dropna(subset=["rent_npr", "size_sqft", "area"])

# असम्भाव्य भाडा सीमित गर्नुहोस् (डाटा प्रविष्टि त्रुटि)
df = df[df["rent_npr"].between(5000, 500000)]
df = df[df["size_sqft"].between(100, 5000)]

print("After cleaning:", df.shape)

तपाईंले सफा गर्दा 5-10% पङ्क्ति गुमाउनुहुनेछ। यो सामान्य छ; कचरा मोडेललाई खुवाउनुभन्दा तिनलाई गुमाउनु राम्रो।

चरण 2 — फिचर इन्जिनियर

डोमेन सहज ज्ञानले सुझाव दिने केही व्युत्पन्न फिचर मद्दत गर्नेछन्:

# दशौं sqft मा आकार (पछि अझ व्याख्यायोग्य गुणक)
df["size_100sqft"] = df["size_sqft"] / 100

# प्रति 100sqft कोठा — अधिक कोठासहितका साना फ्ल्याट भरिएका हुन्छन्
df["rooms_per_100sqft"] = df["bedrooms"] / df["size_100sqft"]

# के यो उच्च-भाडा क्षेत्रमा छ?
high_rent_areas = ["Baluwatar", "Lazimpat", "Bhatbhateni", "Sanepa", "Naxal"]
df["is_high_rent_area"] = df["area"].isin(high_rent_areas).astype(int)

# के यो शीर्ष-तला फ्ल्याट हो? (तला 4+)
df["is_top_floor"] = (df["floor"] >= 4).astype(int)

# पोस्टिङदेखिका दिन — पुराना लिस्टिङमा प्रायः बासी मूल्य हुन्छन्
df["date_posted"] = pd.to_datetime(df["date_posted"])
df["days_since_posted"] = (pd.Timestamp.now() - df["date_posted"]).dt.days

यस्ता डोमेन फिचरमा नै रिग्रेसन मोडेलले आफ्नो प्रतिष्ठा कमाउँछन्। मोडेललाई आफै थाहा छैन कि बालुवाटार महँगो छ; तपाईंले is_high_rent_area फिचर थपेर सिकाउनुहुन्छ।

चरण 3 — बाँड्ने र pipeline

from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline

numeric = [
    "size_100sqft", "bedrooms", "bathrooms",
    "rooms_per_100sqft", "is_top_floor", "is_high_rent_area",
    "days_since_posted",
]
categorical = ["area", "building_type", "furnished", "parking"]

X = df[numeric + categorical]
y = df["rent_npr"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42,
)

model = Pipeline([
    ("preprocess", ColumnTransformer([
        ("num", StandardScaler(), numeric),
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical),
    ])),
    ("regressor", LinearRegression()),
])

model.fit(X_train, y_train)

अहिलेसम्मका हरेक मोडेलजस्तै उही आकार। Pipeline ले लीकेजबिना स्केलिङ र इन्कोडिङ सम्हाल्छ।

चरण 4 — मूल्याङ्कन

import numpy as np
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

predictions = model.predict(X_test)

mae = mean_absolute_error(y_test, predictions)
rmse = np.sqrt(mean_squared_error(y_test, predictions))
r2 = r2_score(y_test, predictions)

print(f"MAE:  Rs {mae:,.0f}")
print(f"RMSE: Rs {rmse:,.0f}")
print(f"R²:   {r2:.3f}")

आउटपुट (चित्रात्मक; तपाईंको फरक हुनेछ):

MAE:  Rs 4,832
RMSE: Rs 7,150
R²:   0.72

परीक्षण लिस्टिङमा मोडेलको सामान्य पूर्वानुमान त्रुटि लगभग रु 4,800 छ। यो लिस्टिङ मूल्य सेट गर्दै गरेको घरधनीका लागि उपयोगी हुन पर्याप्त छ — “रु 4,800 भन्दा फरक” वार्ता दायराभित्र राम्रोसँग छ। 0.72 को R² ले फिचरबाट भाडाको भिन्नताको 72% मोडेलले व्याख्या गर्छ भन्छ। बाँकी 28% हामीले नसमेटेको सबै कुरा हो: भवन उमेर, दृश्य, छिमेक विशेषताहरू, घरधनी प्रतिष्ठा, वर्ष-समय प्रभाव, र अपरिहार्य शोर।

चरण 5 — गुणक पढ्ने

preprocess = model.named_steps["preprocess"]
regressor = model.named_steps["regressor"]
feature_names = preprocess.get_feature_names_out()

# पूर्ण भारद्वारा शीर्ष योगदानकर्ता
weights_with_names = list(zip(feature_names, regressor.coef_))
weights_with_names.sort(key=lambda x: abs(x[1]), reverse=True)

print("Top coefficients:")
for name, w in weights_with_names[:15]:
    print(f"  {name:45s} {w:+12.0f}")
print(f"  {'intercept':45s} {regressor.intercept_:+12.0f}")

तपाईंले यस्तै देख्नुहुनेछ:

  num__size_100sqft                            +6,420
  cat__area_Baluwatar                          +5,180
  cat__area_Lazimpat                           +4,920
  num__is_high_rent_area                       +3,840
  cat__furnished_Fully furnished               +3,500
  cat__area_Kalanki                            -3,800
  num__bedrooms                                +2,150
  ...

यसलाई पढ्नुहोस्। हरेक अतिरिक्त 100 sqft ले औसतमा रु 6,420 थप्छ। बालुवाटार औसतभन्दा रु 5,180 प्रिमियम हो। कलङ्कीमा रु 3,800 छुट हो। पूर्ण रूपमा फर्निच्ड फ्ल्याटले रु 3,500 थप्छ। यी काठमाडौंका भाडाका लागि उचित सङ्ख्या हुन् — मोडेलले स्थानीय बजार सिकेको छ।

एक नयाँ फ्ल्याटका लागि पूर्वानुमान

new_flat = pd.DataFrame([{
    "size_sqft": 850, "bedrooms": 2, "bathrooms": 1,
    "area": "Sanepa", "parking": "Available",
    "furnished": "Semi-furnished", "floor": 3,
    "building_type": "Apartment",
    "date_posted": pd.Timestamp.now(),
}])

# व्युत्पन्न फिचर पुन: गणना (तालिम कै जस्तै)
new_flat["size_100sqft"] = new_flat["size_sqft"] / 100
new_flat["rooms_per_100sqft"] = new_flat["bedrooms"] / new_flat["size_100sqft"]
new_flat["is_high_rent_area"] = new_flat["area"].isin(["Baluwatar", "Lazimpat", "Bhatbhateni", "Sanepa", "Naxal"]).astype(int)
new_flat["is_top_floor"] = (new_flat["floor"] >= 4).astype(int)
new_flat["days_since_posted"] = 0

predicted_rent = model.predict(new_flat[numeric + categorical])[0]
print(f"Predicted rent: Rs {predicted_rent:,.0f}")

मोडेलले Predicted rent: Rs 47,300 जस्तै केही प्रिन्ट गर्नेछ। यसलाई प्रयोग गर्नुहोस्। ±रु 5,000 भित्र विश्वास गर्नुहोस्। यसमा प्रश्न उठाउने कसैलाई परीक्षण सेटमा predicted vs actual देखाउनुहोस्।

residual plot — मोडेल कहाँ खराब छ पक्रने

उपयोगी रिग्रेसन बानी: actual लाई predicted को विरुद्धमा, र actual - predicted (residual) लाई predicted को विरुद्धमा प्लट गर्नुहोस्। residual plot y=0 वरिपरि बिन्दुहरूको विशेषताबिनाको बादल हुनुपर्छ। कुनै संरचनाले मोडेल केही इनपुटका लागि प्रणालीगत रूपमा गलत हो भन्ने सुझाव दिन्छ:

import matplotlib.pyplot as plt

residuals = y_test - predictions

fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].scatter(predictions, y_test, alpha=0.5)
axes[0].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], "k--")
axes[0].set_xlabel("Predicted rent (NPR)")
axes[0].set_ylabel("Actual rent (NPR)")
axes[0].set_title("Actual vs Predicted")

axes[1].scatter(predictions, residuals, alpha=0.5)
axes[1].axhline(0, color="k", linestyle="--")
axes[1].set_xlabel("Predicted rent (NPR)")
axes[1].set_ylabel("Residual (NPR)")
axes[1].set_title("Residuals")
plt.show()

यदि residual plot मा स्पष्ट बाङ्गो छ भने, तपाईंको सम्बन्ध गैर-रेखीय हो र रेखीय मोडेलले चुकाइरहेको छ। यदि यसमा पंखा आकार छ (residuals ठूला पूर्वानुमानका लागि ठूला हुन्छन्) भने, मोडेल heteroscedastic छ र तपाईं log(rent) पूर्वानुमान गर्न चाहन सक्नुहुन्छ। हामी दुवैलाई अर्को खण्डमा भेट्नेछौं।

आफ्नो बुझाइ जाँच्नुहोस्

Quick check

एक टिम सदस्यले ललितपुरका लिस्टिङमा भाडा पूर्वानुमान मोडेल तालिम दिन्छन्। तिनले भक्तपुरमा भाडा पूर्वानुमान गर्न यसलाई तैनात गर्न चाहन्छन्। सबैभन्दा महत्त्वपूर्ण सावधानी के हो?

Quick check

residual plot हेर्दा, तपाईं देख्नुहुन्छ कि ठूला पूर्वानुमानित भाडा (>रु 80,000) मा ठूला सकारात्मक residuals छन् — अर्थात्, वास्तविक भाडा प्रायः पूर्वानुमान गरिएकोभन्दा धेरै उच्च हुन्छन्। यो तपाईंको रेखीय मोडेलबारे के सुझाव दिन्छ?

अब के आउँछ

रेखीय रिग्रेसनले तपाईंलाई धेरै टाढा पुर्‍याउँछ। तर कहिलेकाहीं फिचर र लक्ष्यबीचको सम्बन्ध वास्तवमै बाङ्गो वा अन्तरक्रियात्मक हुन्छ, र रेखीय मोडेलसँग जति फिचर इन्जिनियरिङ गरे पनि यसलाई फिट गर्दैन। अर्को खण्डले दुई गैर-रेखीय रिग्रेसर — KNeighborsRegressorDecisionTreeRegressor — भेट्छ र तपाईंलाई सीधा रेखा पर्याप्त नभएको कहिले चिन्ने देखाउँछ।