Spaces:
Sleeping
Sleeping
File size: 14,911 Bytes
0c8422e fc8451e 0c8422e dde3265 0c8422e fc8451e 0c8422e 3e8e298 0c8422e fc8451e 0c8422e 656590a 0c8422e fc8451e 0c8422e fc8451e 0c8422e 656590a 0c8422e fc8451e 656590a fc8451e 0c8422e fc8451e 0c8422e fc8451e 0c8422e fc8451e 0c8422e fc8451e 0c8422e fc8451e 0c8422e fc8451e 656590a fc8451e 656590a fc8451e 656590a fc8451e 0c8422e 656590a 0c8422e 656590a fc8451e 0c8422e a112534 0c8422e dde3265 0c8422e fc8451e dde3265 0c8422e 656590a fc8451e 0c8422e a112534 0c8422e a112534 0c8422e a112534 fc8451e 0c8422e a112534 656590a 0c8422e 5cdfb21 dde3265 a112534 dde3265 fc8451e 0c8422e a112534 0c8422e a112534 5cdfb21 a112534 fc8451e a112534 d85ce01 a112534 d85ce01 a112534 d85ce01 0c8422e a112534 656590a a112534 0c8422e 656590a 0c8422e 3e8e298 656590a 3e8e298 0c8422e fc8451e 0c8422e fc8451e 0c8422e 3e8e298 fc8451e 0c8422e 3e8e298 656590a 3e8e298 656590a 3e8e298 656590a 3e8e298 656590a a112534 0c8422e 3e8e298 656590a 0c8422e a112534 3e8e298 656590a fc8451e 0c8422e 656590a 3e8e298 656590a 3e8e298 fc8451e 3e8e298 a112534 fc8451e 3e8e298 0c8422e 3e8e298 656590a 3e8e298 8ff1100 0c8422e 3e8e298 656590a 0c8422e 656590a 3e8e298 656590a 3e8e298 0c8422e 3e8e298 656590a a112534 0c8422e 656590a 3e8e298 656590a 0c8422e a112534 0c8422e a112534 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 | import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from datetime import datetime
import prediction_engine as engine
import re
import os
# ==============================================================================
# 1. SMART COLUMN MAPPING (Kamus Sinonim)
# ==============================================================================
# Dictionary ini digunakan untuk mencocokkan nama kolom dari file pengguna
# dengan nama kolom standar yang dibutuhkan sistem.
COLUMN_SYNONYMS = {
'Town': ['town', 'city', 'location', 'kota', 'lokasi', 'area', 'wilayah', 'daerah'],
'Property_Residential': ['property_residential', 'property', 'type', 'tipe', 'jenis', 'kategori', 'category', 'jenis properti'],
'List Year': ['list year', 'year', 'tahun', 'thn', 'tahun daftar', 'listing year'],
'Assessed Value': ['assessed value', 'value', 'price', 'assessed', 'nilai', 'harga', 'taksiran', 'harga taksiran', 'amount', 'rp', 'idr', 'usd']
}
def find_header_row(df_raw):
"""
Mencari baris mana yang mengandung Header secara otomatis.
Berguna jika header Excel ada di baris ke-2 atau ke-3 (bukan baris pertama).
"""
# Cek 10 baris pertama
for i in range(min(10, len(df_raw))):
row_values = df_raw.iloc[i].astype(str).str.lower().tolist()
# Hitung berapa banyak kata kunci 'wajib' yang muncul di baris ini
matches = 0
for key, synonyms in COLUMN_SYNONYMS.items():
if any(syn in " ".join(row_values) for syn in synonyms):
matches += 1
# Jika minimal 3 kolom wajib ditemukan di baris ini, ini adalah Header!
if matches >= 3:
print(f"β
Header ditemukan di baris ke-{i}")
# Set baris ini sebagai header
df_new = df_raw.iloc[i+1:].copy()
df_new.columns = df_raw.iloc[i]
return df_new
# Jika tidak ketemu, kembalikan apa adanya (asumsi baris 0 adalah header)
return df_raw
def normalize_columns(df):
"""
Mengubah nama kolom dari file user menjadi standar sistem.
Contoh: 'Kota' -> 'Town', 'Harga Taksiran' -> 'Assessed Value'
"""
df.columns = df.columns.astype(str).str.strip()
rename_map = {}
# Loop setiap kolom di Dataframe user
for col in df.columns:
col_lower = col.lower().replace('_', ' ').replace('.', ' ')
# Cari kecocokan di kamus sinonim
for standard_col, synonyms in COLUMN_SYNONYMS.items():
# Cek Exact Match atau Partial Match
if any(syn == col_lower or syn in col_lower.split() for syn in synonyms):
if standard_col not in rename_map.values():
rename_map[col] = standard_col
if rename_map:
print(f"π Rename Kolom: {rename_map}")
df = df.rename(columns=rename_map)
return df
def clean_currency_aggressive(x):
"""
Fungsi pembersih angka yang kuat untuk menangani format mata uang.
Bisa membaca: 'Rp 1.500.000', '$ 150,000.00', '150.000', '150,000'
"""
if pd.isna(x) or x == "":
return 0.0
s = str(x).strip()
try:
# Jika formatnya sudah float/int murni, langsung return
if isinstance(x, (int, float)):
return float(x)
# 1. Buang Simbol Mata Uang & Huruf (Rp, $, USD, dll)
# Hanya sisakan angka, titik, koma, dan minus
s = re.sub(r'[^\d.,-]', '', s)
if not s: return 0.0
# 2. Deteksi Format Indonesia (Titik sebagai ribuan) vs US (Koma sebagai ribuan)
# Jika ada titik DAN koma (misal: 150.000,00 atau 150,000.00)
if '.' in s and ',' in s:
if s.rfind('.') < s.rfind(','):
# Format Indo: 150.000,00 -> Titik dihapus, Koma jadi Titik
s = s.replace('.', '').replace(',', '.')
else:
# Format US: 150,000.00 -> Koma dihapus
s = s.replace(',', '')
# Jika hanya ada Titik (misal: 150.000 atau 150.55)
elif '.' in s:
# Jika titik muncul lebih dari sekali (1.000.000), itu pasti ribuan -> Hapus
if s.count('.') > 1:
s = s.replace('.', '')
# Jika titik cuma satu tapi di akhir (100. -> 100)
elif s.endswith('.'):
s = s.replace('.', '')
# Asumsi input properti angka bulat besar: Hapus titik jika terlihat seperti ribuan
elif len(s.split('.')[-1]) == 3:
s = s.replace('.', '')
# Jika hanya ada Koma (misal: 150,000) -> Hapus koma
elif ',' in s:
s = s.replace(',', '')
return float(s)
except:
return 0.0
# ==============================================================================
# 2. ROBUST PLOTTING (Visualisasi Data)
# ==============================================================================
def safe_generate_plots(df):
"""
Membuat 9 grafik visualisasi menggunakan Plotly.
Fungsi ini aman (safe), artinya jika data kosong, akan mengembalikan grafik kosong, bukan error.
"""
if df is None or df.empty:
empty = go.Figure().update_layout(
title="Data Kosong / Gagal Membaca Angka",
xaxis={"visible":False}, yaxis={"visible":False},
annotations=[{"text": "Cek Format Angka Excel Anda", "showarrow":False, "font":{"size":20}}]
)
return [empty] * 9
# 1. Scatter: Prediksi vs Nilai Taksiran
try:
fig1 = px.scatter(
df, x="Assessed Value", y="Predicted_Sale_Amount", color="Property_Residential",
hover_data=['Town', 'List Year'], title="π Prediksi vs Nilai Taksiran",
template="plotly_white", opacity=0.8
)
fig1.update_traces(marker=dict(size=12)) # Memperbesar ukuran titik
except: fig1 = go.Figure()
# 2. Box Plot: Sebaran Harga (Restored)
try:
top_towns = df['Town'].value_counts().nlargest(10).index
df_top = df[df['Town'].isin(top_towns)]
fig2 = px.box(
df_top, x="Town", y="Predicted_Sale_Amount", color="Town",
title="ποΈ Sebaran Harga (Price Distribution per Town)",
template="plotly_white", points="all"
)
fig2.update_layout(showlegend=False)
except: fig2 = go.Figure()
# 3. Line Chart: Trend (Dual Axis)
# Menampilkan Assessed Value dan Predicted Price dalam satu grafik dengan dua skala sumbu Y.
try:
df_line = df.sort_values(by="Assessed Value").reset_index(drop=True)
fig3 = make_subplots(specs=[[{"secondary_y": True}]])
fig3.add_trace(go.Scatter(x=df_line.index, y=df_line['Assessed Value'], mode='lines', name='Assessed Value', line=dict(color='orange')), secondary_y=False)
fig3.add_trace(go.Scatter(x=df_line.index, y=df_line['Predicted_Sale_Amount'], mode='lines', name='Predicted Price', line=dict(color='blue')), secondary_y=True)
fig3.update_layout(title="π Predicted vs Assessed Value Trend (Dual Scale)", xaxis_title="Property Index (Sorted by Value)", template="plotly_white")
fig3.update_yaxes(title_text="Assessed Value ($)", secondary_y=False)
fig3.update_yaxes(title_text="Predicted Price ($)", secondary_y=True)
except: fig3 = go.Figure()
# 4. Violin Plot: Price Distribution Top 5 (Restored)
try:
top_5_towns = df['Town'].value_counts().nlargest(5).index
df_dist = df[df['Town'].isin(top_5_towns)]
fig4 = px.violin(
df_dist, x="Predicted_Sale_Amount", y="Town", orientation='h',
box=True, points="all", color="Town",
title="π» Price Distribution by Town (Top 5)",
template="plotly_white"
)
fig4.update_layout(showlegend=False)
except: fig4 = go.Figure()
# 5. Heatmap: Town vs Type (Restored)
try:
heatmap_data = df.groupby(['Town', 'Property_Residential'])['Predicted_Sale_Amount'].mean().reset_index()
top_towns = df['Town'].value_counts().nlargest(20).index
heatmap_data = heatmap_data[heatmap_data['Town'].isin(top_towns)]
fig5 = px.density_heatmap(
heatmap_data, x="Town", y="Property_Residential", z="Predicted_Sale_Amount",
histfunc="avg", title="π₯ Avg Price Heatmap (Town vs Type)",
color_continuous_scale="Viridis", template="plotly_white"
)
except: fig5 = go.Figure()
# 6. Bar Chart: Top Growth Towns
try:
df['Ratio'] = df['Predicted_Sale_Amount'] / df['Assessed Value']
growth = df.groupby("Town")['Ratio'].mean().reset_index()
growth = growth.sort_values(by="Ratio", ascending=False).head(10)
fig6 = px.bar(
growth, x="Ratio", y="Town", orientation='h',
title="π Top 10 High Growth Towns (Avg Ratio)",
color="Ratio", color_continuous_scale="RdBu", template="plotly_white"
)
fig6.add_vline(x=1.0, line_dash="dash", line_color="black")
except: fig6 = go.Figure()
# 7. Pie Chart: Price Segmentation (New)
try:
mean_val = df['Predicted_Sale_Amount'].mean()
def segment(x):
if x < mean_val * 0.8: return 'Budget'
elif x > mean_val * 1.5: return 'Luxury'
else: return 'Mid-Range'
df['Segment'] = df['Predicted_Sale_Amount'].apply(segment)
fig7 = px.pie(
df, names='Segment', values='Predicted_Sale_Amount',
title="π° Price Segmentation (Value Share)",
template="plotly_white", hole=0.4
)
except: fig7 = go.Figure()
# 8. Scatter: Outlier Detection (>100% threshold)
try:
df['Diff_Pct'] = ((df['Predicted_Sale_Amount'] - df['Assessed Value']) / df['Assessed Value']) * 100
# Threshold: > 100% perbedaan
df['Is_Outlier'] = df['Diff_Pct'].abs() > 100
fig8 = px.scatter(
df, x="Assessed Value", y="Diff_Pct", color="Is_Outlier",
hover_data=['Town', 'Property_Residential'],
title="β οΈ Outlier Detection (> 100% Difference)",
labels={"Diff_Pct": "Difference (%)", "Is_Outlier": "Is Outlier?"},
template="plotly_white", color_discrete_map={True: 'red', False: 'gray'}
)
fig8.add_hline(y=100, line_dash="dash", line_color="red")
fig8.add_hline(y=-100, line_dash="dash", line_color="red")
except: fig8 = go.Figure()
# 9. Bar Chart: Investment Potential (New)
try:
df['Potential_Upside'] = df['Predicted_Sale_Amount'] - df['Assessed Value']
top_invest = df.nlargest(10, 'Potential_Upside')
fig9 = px.bar(
top_invest, x="Town", y="Potential_Upside", color="Property_Residential",
hover_data=['Predicted_Sale_Amount'],
title="π Investment Potential (Top Undervalued)",
labels={"Potential_Upside": "Potential Gain ($)"},
template="plotly_white"
)
except: fig9 = go.Figure()
return fig1, fig2, fig3, fig4, fig5, fig6, fig7, fig8, fig9
# ==============================================================================
# 3. PIPELINE UTAMA (Proses Data)
# ==============================================================================
def load_preview(file_obj):
"""
Memuat file (Excel/CSV) dan mengembalikan dataframe untuk preview.
"""
if file_obj is None:
return None, "No file uploaded."
try:
filename = file_obj.name.lower()
if filename.endswith('.csv'):
df = pd.read_csv(file_obj.name, header=None)
elif filename.endswith(('.xlsx', '.xls')):
df = pd.read_excel(file_obj.name, header=None)
else:
return None, "Format file tidak didukung. Gunakan .xlsx atau .csv"
df = find_header_row(df)
df = normalize_columns(df)
# Proses awal minimal untuk preview
if 'Assessed Value' in df.columns:
# Bersihkan mata uang tapi biarkan string/float untuk sementara
df['Assessed Value'] = df['Assessed Value'].apply(clean_currency_aggressive)
return df, None
except Exception as e:
return None, f"Error reading file: {str(e)}"
def process_dataframe(df):
"""
Memproses dataframe utama: Cleaning, Prediksi, dan Visualisasi.
"""
if df is None or df.empty:
# Kembalikan None untuk 9 figure
return "ERROR_GENERIC", "Dataframe kosong.", None, None, None, None, None, None, None, None, None
try:
# Cek kelengkapan kolom
req_cols = ['Town', 'Property_Residential', 'List Year', 'Assessed Value']
missing = [c for c in req_cols if c not in df.columns]
if missing:
return "ERROR_COLS", f"Kolom hilang: {missing}", None, None, None, None, None, None, None, None, None
# Konversi Tahun
df['List Year'] = pd.to_numeric(df['List Year'], errors='coerce').fillna(2021).astype(int).astype(str)
# Pastikan Assessed Value numerik
df['Assessed Value'] = pd.to_numeric(df['Assessed Value'], errors='coerce').fillna(0.0)
# Filter baris yang valid (Value > 0)
df_valid = df[df['Assessed Value'] > 0].copy()
if df_valid.empty:
return "ERROR_COLS", "Tidak ada data dengan Assessed Value > 0.", None, None, None, None, None, None, None, None, None
if 'Prediction Date' not in df_valid.columns:
df_valid['Prediction Date'] = datetime.now().strftime('%Y-%m-%d')
preds = []
# Loop prediksi per baris
for _, row in df_valid.iterrows():
val = engine.predict_single(
row['Assessed Value'],
row['Town'],
row['Property_Residential'],
row['List Year'],
str(row['Prediction Date']),
return_debug=False
)
preds.append(round(val, 3))
df_valid['Predicted_Sale_Amount'] = preds
# Hitung Metrics & Diff
metrics = engine.calculate_key_metrics(df_valid)
if 'Diff' in df_valid.columns:
df_valid['Diff'] = df_valid['Diff'].round(3)
# Generate 9 Plots
fig1, fig2, fig3, fig4, fig5, fig6, fig7, fig8, fig9 = safe_generate_plots(df_valid)
# Simpan hasil ke Excel
out_file = "analysis_result.xlsx"
df_valid.to_excel(out_file, index=False)
return df_valid, metrics, fig1, fig2, fig3, fig4, fig5, fig6, fig7, fig8, fig9, out_file
except Exception as e:
import traceback
traceback.print_exc()
return "ERROR_GENERIC", str(e), None, None, None, None, None, None, None, None, None
|