Brian045 commited on
Commit
fc8451e
ยท
verified ยท
1 Parent(s): bcde870

Update data_handler.py

Browse files
Files changed (1) hide show
  1. data_handler.py +149 -92
data_handler.py CHANGED
@@ -1,126 +1,173 @@
1
  import pandas as pd
 
2
  import plotly.express as px
3
  import plotly.graph_objects as go
4
  from datetime import datetime
5
- import prediction_engine as engine # Kita panggil engine untuk prediksi
6
  import re
7
 
8
  # ==============================================================================
9
- # 1. SMART COLUMN MAPPING (AUTO-ADJUST)
10
  # ==============================================================================
11
- # Ini kamus sinonim. Sistem akan mencari kolom yg mirip dengan kata-kata ini.
12
  COLUMN_SYNONYMS = {
13
- 'Town': ['town', 'city', 'location', 'kota', 'lokasi', 'area'],
14
- 'Property_Residential': ['property_residential', 'property type', 'type', 'tipe', 'jenis properti', 'category'],
15
- 'List Year': ['list year', 'list_year', 'year', 'tahun', 'tahun daftar'],
16
- 'Assessed Value': ['assessed value', 'assessed_value', 'value', 'price', 'assessed', 'nilai', 'harga', 'taksiran']
17
  }
18
 
19
- def normalize_columns(df):
20
  """
21
- Mengubah nama kolom user yang aneh-aneh menjadi standar sistem.
22
- Contoh: User upload 'Kota' -> Sistem ubah jadi 'Town'
23
  """
24
- df.columns = df.columns.str.strip() # Hapus spasi di nama kolom
25
-
26
- # Buat map dari nama kolom asli ke nama kolom standar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  rename_map = {}
 
 
28
  for col in df.columns:
29
- col_lower = col.lower().replace('_', ' ')
30
 
 
31
  for standard_col, synonyms in COLUMN_SYNONYMS.items():
32
- # Cek jika nama kolom mengandung salah satu sinonim
33
- if any(syn in col_lower for syn in synonyms):
34
- # Prioritas: Jangan timpa jika sudah ada yang pas
35
  if standard_col not in rename_map.values():
36
  rename_map[col] = standard_col
37
 
38
- # Lakukan rename
39
  if rename_map:
40
- print(f"๐Ÿ”„ Auto-Adjusting Columns: {rename_map}")
41
  df = df.rename(columns=rename_map)
42
 
43
  return df
44
 
45
- def clean_currency(x):
46
- """Membersihkan data angka yang terformat string (misal: '$ 150,000')"""
47
- if isinstance(x, str):
48
- # Hapus simbol $ , dan spasi
49
- clean = re.sub(r'[^\d.]', '', x)
50
- try:
51
- return float(clean)
52
- except:
53
- return 0.0
54
- return x
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  # ==============================================================================
57
- # 2. ROBUST PLOTTING (ANTI ERROR)
58
  # ==============================================================================
59
  def safe_generate_plots(df):
60
- """
61
- Membuat grafik dengan pengecekan data kosong agar tidak error TypeError.
62
- """
63
- # Cek Data Kosong
64
- if df is None or df.empty or len(df) == 0:
65
- # Return grafik kosong dengan tulisan "No Data"
66
- empty_fig = go.Figure()
67
- empty_fig.update_layout(
68
- xaxis={"visible": False}, yaxis={"visible": False},
69
- annotations=[{"text": "Data Kosong / Tidak Valid", "showarrow": False, "font": {"size": 20}}]
70
  )
71
- return empty_fig, empty_fig, empty_fig, empty_fig
72
 
73
- # 1. Scatter Plot
74
  try:
75
  fig1 = px.scatter(
76
  df, x="Assessed Value", y="Predicted_Sale_Amount", color="Property_Residential",
77
- hover_data=['Town', 'List Year'], title="๐Ÿ“ˆ Korelasi: Nilai Taksiran vs Prediksi",
78
  template="plotly_white", opacity=0.7
79
  )
80
- max_val = max(df["Assessed Value"].max(), df["Predicted_Sale_Amount"].max())
81
- fig1.add_shape(type="line", x0=0, y0=0, x1=max_val, y1=max_val, line=dict(color="Red", width=2, dash="dash"))
82
- except:
83
- fig1 = go.Figure().update_layout(title="Gagal Render Scatter")
84
 
85
- # 2. Box Plot
86
  try:
87
- top_towns = df['Town'].value_counts().nlargest(15).index
88
  fig2 = px.box(
89
- df[df['Town'].isin(top_towns)], x="Town", y="Predicted_Sale_Amount", color="Town",
90
- title="๐Ÿ™๏ธ Sebaran Harga Kota", template="plotly_white"
91
  )
92
  fig2.update_layout(showlegend=False)
93
- except:
94
- fig2 = go.Figure().update_layout(title="Gagal Render Boxplot")
95
 
96
- # 3. Histogram Ratio (DIPERBAIKI DARI KODE SEBELUMNYA)
97
  try:
98
- df_valid = df[df['Assessed Value'] > 0].copy()
99
- if not df_valid.empty:
100
- df_valid['Ratio'] = df_valid['Predicted_Sale_Amount'] / df_valid['Assessed Value']
101
  fig3 = px.histogram(
102
- df_valid, x="Ratio", nbins=30, title="๐Ÿ“Š Distribusi Rasio",
103
  color_discrete_sequence=['#00CC96'], template="plotly_white"
104
  )
105
  fig3.add_vline(x=1.0, line_dash="dash", line_color="red")
106
  else:
107
- # FIX: Jangan pakai px.histogram tanpa data. Pakai go.Figure kosong.
108
- fig3 = go.Figure()
109
- fig3.update_layout(title="Assessed Value 0 semua", xaxis={"visible":False}, yaxis={"visible":False})
110
- fig3.add_annotation(text="Tidak bisa hitung rasio (Assessed Value = 0)", showarrow=False)
111
- except Exception as e:
112
- print(f"Plot Error: {e}")
113
- fig3 = go.Figure().update_layout(title="Error Histogram")
114
 
115
- # 4. Bar Chart
116
  try:
117
- val_by_type = df.groupby("Property_Residential")["Predicted_Sale_Amount"].sum().reset_index()
118
- fig4 = px.bar(
119
- val_by_type, x="Predicted_Sale_Amount", y="Property_Residential", orientation='h',
120
- title="๐Ÿ’ฐ Valuasi per Tipe", text_auto='.2s', color="Predicted_Sale_Amount", template="plotly_white"
121
- )
122
- except:
123
- fig4 = go.Figure().update_layout(title="Gagal Render Bar Chart")
124
 
125
  return fig1, fig2, fig3, fig4
126
 
@@ -131,35 +178,48 @@ def process_file_pipeline(file_obj):
131
  if file_obj is None: return None
132
 
133
  try:
134
- # A. Baca File (Flexible Excel/CSV)
135
  filename = file_obj.name.lower()
136
  if filename.endswith('.csv'):
137
- df = pd.read_csv(file_obj.name)
138
  elif filename.endswith(('.xlsx', '.xls')):
139
- df = pd.read_excel(file_obj.name)
140
  else:
141
- return "ERROR_FORMAT", "Format harus .csv atau .xlsx", None, None, None, None, None
 
 
 
142
 
143
- # B. Normalisasi Kolom (Fitur Auto-Adjust)
144
  df = normalize_columns(df)
145
 
146
- # C. Validasi Kolom Standar
147
  req_cols = ['Town', 'Property_Residential', 'List Year', 'Assessed Value']
148
  missing = [c for c in req_cols if c not in df.columns]
149
-
150
  if missing:
151
- # Pesan error lebih ramah
152
- return "ERROR_COLS", f"Sistem tidak mengenali kolom: {', '.join(missing)}. \n(Coba ganti nama header di Excel Anda)", None, None, None, None, None
 
 
 
 
153
 
154
- # D. Cleaning Data
155
- df['Assessed Value'] = df['Assessed Value'].apply(clean_currency)
156
- df = df.dropna(subset=['Assessed Value']) # Hapus baris kosong
157
 
158
- # Tambah tanggal default
 
 
 
 
 
 
 
 
159
  if 'Prediction Date' not in df.columns:
160
  df['Prediction Date'] = datetime.now().strftime('%Y-%m-%d')
161
 
162
- # E. Loop Prediksi (Panggil Engine)
163
  preds = []
164
  for _, row in df.iterrows():
165
  val = engine.predict_single(
@@ -173,12 +233,9 @@ def process_file_pipeline(file_obj):
173
 
174
  df['Predicted_Sale_Amount'] = preds
175
 
176
- # F. Generate Metrics & Plots
177
- # Pakai engine untuk hitung metrics, tapi plot pakai fungsi safe di sini
178
- metrics = engine.calculate_key_metrics(df)
179
  fig1, fig2, fig3, fig4 = safe_generate_plots(df)
180
-
181
- # G. Save & Return
182
  out_csv = "analysis_result.csv"
183
  df.to_csv(out_csv, index=False)
184
 
 
1
  import pandas as pd
2
+ import numpy as np
3
  import plotly.express as px
4
  import plotly.graph_objects as go
5
  from datetime import datetime
6
+ import prediction_engine as engine
7
  import re
8
 
9
  # ==============================================================================
10
+ # 1. SMART COLUMN MAPPING (Kamus Sinonim)
11
  # ==============================================================================
 
12
  COLUMN_SYNONYMS = {
13
+ 'Town': ['town', 'city', 'location', 'kota', 'lokasi', 'area', 'wilayah', 'daerah'],
14
+ 'Property_Residential': ['property_residential', 'property', 'type', 'tipe', 'jenis', 'kategori', 'category', 'jenis properti'],
15
+ 'List Year': ['list year', 'year', 'tahun', 'thn', 'tahun daftar', 'listing year'],
16
+ 'Assessed Value': ['assessed value', 'value', 'price', 'assessed', 'nilai', 'harga', 'taksiran', 'harga taksiran', 'amount', 'rp', 'idr', 'usd']
17
  }
18
 
19
+ def find_header_row(df_raw):
20
  """
21
+ Mencari baris mana yang mengandung Header.
22
+ Berguna jika header Excel ada di baris ke-2 atau ke-3.
23
  """
24
+ # Cek 10 baris pertama
25
+ for i in range(min(10, len(df_raw))):
26
+ row_values = df_raw.iloc[i].astype(str).str.lower().tolist()
27
+ # Hitung berapa banyak kata kunci 'wajib' yang muncul di baris ini
28
+ matches = 0
29
+ for key, synonyms in COLUMN_SYNONYMS.items():
30
+ if any(syn in " ".join(row_values) for syn in synonyms):
31
+ matches += 1
32
+
33
+ # Jika minimal 3 kolom wajib ditemukan di baris ini, ini adalah Header!
34
+ if matches >= 3:
35
+ print(f"โœ… Header ditemukan di baris ke-{i}")
36
+ # Set baris ini sebagai header
37
+ df_new = df_raw.iloc[i+1:].copy()
38
+ df_new.columns = df_raw.iloc[i]
39
+ return df_new
40
+
41
+ # Jika tidak ketemu, kembalikan apa adanya (asumsi baris 0 adalah header)
42
+ return df_raw
43
+
44
+ def normalize_columns(df):
45
+ """Mengubah nama kolom user menjadi standar sistem."""
46
+ df.columns = df.columns.astype(str).str.strip()
47
  rename_map = {}
48
+
49
+ # Loop setiap kolom di Dataframe user
50
  for col in df.columns:
51
+ col_lower = col.lower().replace('_', ' ').replace('.', ' ')
52
 
53
+ # Cari kecocokan di kamus sinonim
54
  for standard_col, synonyms in COLUMN_SYNONYMS.items():
55
+ # Cek Exact Match atau Partial Match
56
+ if any(syn == col_lower or syn in col_lower.split() for syn in synonyms):
 
57
  if standard_col not in rename_map.values():
58
  rename_map[col] = standard_col
59
 
 
60
  if rename_map:
61
+ print(f"๐Ÿ”„ Rename Kolom: {rename_map}")
62
  df = df.rename(columns=rename_map)
63
 
64
  return df
65
 
66
+ def clean_currency_aggressive(x):
67
+ """
68
+ Pembersih angka SUPER KUAT.
69
+ Bisa baca: 'Rp 1.500.000', '$ 150,000.00', '150.000', '150,000'
70
+ """
71
+ if pd.isna(x) or x == "":
72
+ return 0.0
73
+
74
+ s = str(x).strip()
75
+
76
+ try:
77
+ # Jika formatnya sudah float/int murni
78
+ if isinstance(x, (int, float)):
79
+ return float(x)
80
+
81
+ # 1. Buang Simbol Mata Uang & Huruf (Rp, $, USD, dll)
82
+ # Hanya sisakan angka, titik, koma, dan minus
83
+ s = re.sub(r'[^\d.,-]', '', s)
84
+
85
+ if not s: return 0.0
86
+
87
+ # 2. Deteksi Format Indonesia (Titik sebagai ribuan) vs US (Koma sebagai ribuan)
88
+ # Jika ada titik DAN koma (misal: 150.000,00 atau 150,000.00)
89
+ if '.' in s and ',' in s:
90
+ if s.rfind('.') < s.rfind(','):
91
+ # Format Indo: 150.000,00 -> Titik dihapus, Koma jadi Titik
92
+ s = s.replace('.', '').replace(',', '.')
93
+ else:
94
+ # Format US: 150,000.00 -> Koma dihapus
95
+ s = s.replace(',', '')
96
+
97
+ # Jika hanya ada Titik (misal: 150.000 atau 150.55)
98
+ elif '.' in s:
99
+ # Jika titik muncul lebih dari sekali (1.000.000), itu pasti ribuan -> Hapus
100
+ if s.count('.') > 1:
101
+ s = s.replace('.', '')
102
+ # Jika titik cuma satu tapi di akhir (100. -> 100)
103
+ elif s.endswith('.'):
104
+ s = s.replace('.', '')
105
+ # Cek panjang desimal. Jika 3 digit persis setelah titik (150.000), kemungkinan besar ribuan
106
+ # TAPI ini ambigu. Kita asumsikan user properti input angka bulat besar.
107
+ # Amannya: Jika hasilnya nanti < 1000 dan input aslinya panjang, berarti salah baca.
108
+ # Mari gunakan pendekatan 'pd.to_numeric' yang lebih pintar nanti.
109
+ # Untuk sekarang: Hapus titik jika terlihat seperti ribuan
110
+ elif len(s.split('.')[-1]) == 3:
111
+ s = s.replace('.', '')
112
+
113
+ # Jika hanya ada Koma (misal: 150,000) -> Hapus koma
114
+ elif ',' in s:
115
+ s = s.replace(',', '')
116
+
117
+ return float(s)
118
+ except:
119
+ return 0.0
120
 
121
  # ==============================================================================
122
+ # 2. ROBUST PLOTTING (Agar tidak Error "NoneType")
123
  # ==============================================================================
124
  def safe_generate_plots(df):
125
+ if df is None or df.empty:
126
+ empty = go.Figure().update_layout(
127
+ title="Data Kosong / Gagal Membaca Angka",
128
+ xaxis={"visible":False}, yaxis={"visible":False},
129
+ annotations=[{"text": "Cek Format Angka Excel Anda", "showarrow":False, "font":{"size":20}}]
 
 
 
 
 
130
  )
131
+ return empty, empty, empty, empty
132
 
133
+ # 1. Scatter
134
  try:
135
  fig1 = px.scatter(
136
  df, x="Assessed Value", y="Predicted_Sale_Amount", color="Property_Residential",
137
+ hover_data=['Town', 'List Year'], title="๐Ÿ“ˆ Prediksi vs Nilai Taksiran",
138
  template="plotly_white", opacity=0.7
139
  )
140
+ except: fig1 = go.Figure()
 
 
 
141
 
142
+ # 2. Box
143
  try:
144
+ top = df['Town'].value_counts().nlargest(10).index
145
  fig2 = px.box(
146
+ df[df['Town'].isin(top)], x="Town", y="Predicted_Sale_Amount", color="Town",
147
+ title="๐Ÿ™๏ธ Sebaran Harga", template="plotly_white"
148
  )
149
  fig2.update_layout(showlegend=False)
150
+ except: fig2 = go.Figure()
 
151
 
152
+ # 3. Histogram
153
  try:
154
+ df_ok = df[df['Assessed Value'] > 100] # Hanya data valid
155
+ if not df_ok.empty:
156
+ df_ok['Ratio'] = df_ok['Predicted_Sale_Amount'] / df_ok['Assessed Value']
157
  fig3 = px.histogram(
158
+ df_ok, x="Ratio", nbins=30, title="๐Ÿ“Š Rasio Kenaikan",
159
  color_discrete_sequence=['#00CC96'], template="plotly_white"
160
  )
161
  fig3.add_vline(x=1.0, line_dash="dash", line_color="red")
162
  else:
163
+ fig3 = go.Figure().update_layout(title="Data Value Kosong")
164
+ except: fig3 = go.Figure()
 
 
 
 
 
165
 
166
+ # 4. Bar
167
  try:
168
+ val = df.groupby("Property_Residential")["Predicted_Sale_Amount"].sum().reset_index()
169
+ fig4 = px.bar(val, x="Predicted_Sale_Amount", y="Property_Residential", orientation='h', title="๐Ÿ’ฐ Total Aset")
170
+ except: fig4 = go.Figure()
 
 
 
 
171
 
172
  return fig1, fig2, fig3, fig4
173
 
 
178
  if file_obj is None: return None
179
 
180
  try:
181
+ # A. BACA FILE (Header=None dulu supaya bisa cari header manual)
182
  filename = file_obj.name.lower()
183
  if filename.endswith('.csv'):
184
+ df = pd.read_csv(file_obj.name, header=None)
185
  elif filename.endswith(('.xlsx', '.xls')):
186
+ df = pd.read_excel(file_obj.name, header=None)
187
  else:
188
+ return "ERROR_FORMAT", "Format salah", None, None, None, None, None
189
+
190
+ # B. CARI HEADER ASLI
191
+ df = find_header_row(df)
192
 
193
+ # C. RENAMING KOLOM
194
  df = normalize_columns(df)
195
 
196
+ # D. CEK KOLOM WAJIB
197
  req_cols = ['Town', 'Property_Residential', 'List Year', 'Assessed Value']
198
  missing = [c for c in req_cols if c not in df.columns]
 
199
  if missing:
200
+ col_list = ", ".join(df.columns.tolist())
201
+ return "ERROR_COLS", f"Kolom tidak ditemukan.\nKolom terbaca: {col_list}.\nKurang: {missing}", None, None, None, None, None
202
+
203
+ # E. DATA CLEANING & TYPE CONVERSION
204
+ # Bersihkan Assessed Value dengan Agresif
205
+ df['Assessed Value'] = df['Assessed Value'].apply(clean_currency_aggressive)
206
 
207
+ # Bersihkan List Year
208
+ df['List Year'] = pd.to_numeric(df['List Year'], errors='coerce').fillna(2021).astype(int).astype(str)
 
209
 
210
+ # Filter: Hapus jika Value 0 atau NaN
211
+ initial_len = len(df)
212
+ df = df[df['Assessed Value'] > 0].copy()
213
+ final_len = len(df)
214
+
215
+ if df.empty:
216
+ return "ERROR_COLS", f"Data terbaca tapi nilainya 0 semua. Cek format angka Excel. (Baris awal: {initial_len}, Baris valid: 0)", None, None, None, None, None
217
+
218
+ # Tambah Tanggal Default
219
  if 'Prediction Date' not in df.columns:
220
  df['Prediction Date'] = datetime.now().strftime('%Y-%m-%d')
221
 
222
+ # F. LOOP PREDIKSI
223
  preds = []
224
  for _, row in df.iterrows():
225
  val = engine.predict_single(
 
233
 
234
  df['Predicted_Sale_Amount'] = preds
235
 
236
+ # G. HASIL
237
+ metrics = engine.calculate_key_metrics(df)
 
238
  fig1, fig2, fig3, fig4 = safe_generate_plots(df)
 
 
239
  out_csv = "analysis_result.csv"
240
  df.to_csv(out_csv, index=False)
241