streamlit-launcher 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,848 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import streamlit as st
4
+ import plotly.graph_objects as go
5
+ import plotly.express as px
6
+ import matplotlib.pyplot as plt
7
+ from scipy import stats
8
+ import matplotlib.dates as mdates
9
+ from datetime import datetime, timedelta
10
+ import warnings
11
+ warnings.filterwarnings('ignore')
12
+
13
+ # Konfigurasi halaman
14
+ st.set_page_config(
15
+ page_title="Dashboard Statistik Profesional",
16
+ page_icon="📊",
17
+ layout="wide",
18
+ initial_sidebar_state="expanded"
19
+ )
20
+
21
+ # CSS kustom untuk styling
22
+ st.markdown("""
23
+ <style>
24
+ .main-header {
25
+ font-size: 3rem;
26
+ color: #1f77b4;
27
+ text-align: center;
28
+ margin-bottom: 2rem;
29
+ }
30
+ .metric-card {
31
+ background-color: #f0f2f6;
32
+ padding: 1.5rem;
33
+ border-radius: 0.5rem;
34
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
35
+ }
36
+ .stButton>button {
37
+ width: 100%;
38
+ background-color: #1f77b4;
39
+ color: white;
40
+ }
41
+ .data-table {
42
+ width: 100%;
43
+ border-collapse: collapse;
44
+ margin: 1rem 0;
45
+ }
46
+ .data-table th, .data-table td {
47
+ border: 1px solid #ddd;
48
+ padding: 8px;
49
+ text-align: left;
50
+ }
51
+ .data-table th {
52
+ background-color: #f2f2f2;
53
+ }
54
+ .data-table tr:nth-child(even) {
55
+ background-color: #f9f9f9;
56
+ }
57
+ .up-trend {
58
+ color: green;
59
+ font-weight: bold;
60
+ }
61
+ .down-trend {
62
+ color: red;
63
+ font-weight: bold;
64
+ }
65
+ .stock-summary {
66
+ background-color: #f8f9fa;
67
+ padding: 15px;
68
+ border-radius: 5px;
69
+ margin-bottom: 20px;
70
+ border-left: 5px solid #1f77b4;
71
+ }
72
+ </style>
73
+ """, unsafe_allow_html=True)
74
+
75
+ # Fungsi untuk menampilkan dataframe sebagai HTML (menggantikan st.dataframe)
76
+ def display_dataframe(df, num_rows=5):
77
+ st.markdown(df.head(num_rows).to_html(classes='data-table', escape=False), unsafe_allow_html=True)
78
+
79
+ # Fungsi untuk memproses file yang diupload
80
+ def process_uploaded_file(uploaded_file):
81
+ try:
82
+ if uploaded_file.name.endswith('.csv'):
83
+ df = pd.read_csv(uploaded_file)
84
+ elif uploaded_file.name.endswith('.xlsx') or uploaded_file.name.endswith('.xls'):
85
+ df = pd.read_excel(uploaded_file)
86
+ else:
87
+ st.error("Format file tidak didukung. Harap unggah file CSV atau Excel.")
88
+ return None
89
+
90
+ return df
91
+ except Exception as e:
92
+ st.error(f"Terjadi kesalahan saat memproses file: {str(e)}")
93
+ return None
94
+
95
+ # Fungsi untuk menggabungkan beberapa dataset
96
+ def merge_datasets(datasets, merge_method='concat'):
97
+ if not datasets:
98
+ return None
99
+
100
+ if merge_method == 'concat':
101
+ # Gabungkan dataset secara vertikal (menambah baris)
102
+ return pd.concat(datasets, ignore_index=True)
103
+ else:
104
+ # Untuk penggabungan berdasarkan kolom (join)
105
+ merged_df = datasets[0]
106
+ for i in range(1, len(datasets)):
107
+ merged_df = pd.merge(merged_df, datasets[i], how=merge_method)
108
+ return merged_df
109
+
110
+ # Fungsi untuk membuat visualisasi data
111
+ def create_visualizations(df):
112
+ # Menentukan kolom numerik dan non-numerik
113
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
114
+ non_numeric_cols = df.select_dtypes(exclude=[np.number]).columns.tolist()
115
+
116
+ # Jika tidak ada kolom numerik, beri pesan error
117
+ if not numeric_cols:
118
+ st.error("Tidak ditemukan kolom numerik dalam dataset.")
119
+ return
120
+
121
+ # Default kolom untuk sumbu X dan Y
122
+ default_x = non_numeric_cols[0] if non_numeric_cols else df.index.name if df.index.name else "Index"
123
+ default_y = numeric_cols[0]
124
+
125
+ # Sidebar untuk konfigurasi chart
126
+ st.sidebar.header("Konfigurasi Visualisasi")
127
+
128
+ # Pilihan jenis chart
129
+ chart_type = st.sidebar.selectbox(
130
+ "Pilih Jenis Chart",
131
+ ["Line Chart", "Bar Chart", "Histogram", "Scatter Plot", "Pie Chart", "Box Plot", "Heatmap", "Candlestick", "Area Chart", "Histogram dengan Multiple Variables"]
132
+ )
133
+
134
+ # Pilihan kolom berdasarkan jenis chart
135
+ if chart_type in ["Line Chart", "Bar Chart", "Scatter Plot", "Area Chart"]:
136
+ x_col = st.sidebar.selectbox("Pilih kolom untuk sumbu X", [default_x] + non_numeric_cols + numeric_cols)
137
+ y_col = st.sidebar.selectbox("Pilih kolom untuk sumbu Y", numeric_cols, index=0)
138
+
139
+ if chart_type == "Line Chart":
140
+ fig = px.line(df, x=x_col, y=y_col, title=f"{y_col} over {x_col}")
141
+ elif chart_type == "Bar Chart":
142
+ fig = px.bar(df, x=x_col, y=y_col, title=f"{y_col} by {x_col}")
143
+ elif chart_type == "Scatter Plot":
144
+ color_col = st.sidebar.selectbox("Pilih kolom untuk warna (opsional)", [None] + non_numeric_cols + numeric_cols)
145
+ size_col = st.sidebar.selectbox("Pilih kolom untuk ukuran (opsional)", [None] + numeric_cols)
146
+ fig = px.scatter(df, x=x_col, y=y_col, color=color_col, size=size_col,
147
+ title=f"Scatter Plot: {y_col} vs {x_col}")
148
+ elif chart_type == "Area Chart":
149
+ fig = px.area(df, x=x_col, y=y_col, title=f"{y_col} over {x_col} (Area Chart)")
150
+
151
+ elif chart_type == "Histogram":
152
+ col = st.sidebar.selectbox("Pilih kolom", numeric_cols)
153
+ fig = px.histogram(df, x=col, title=f"Distribusi {col}")
154
+
155
+ elif chart_type == "Histogram dengan Multiple Variables":
156
+ selected_cols = st.sidebar.multiselect("Pilih kolom untuk histogram", numeric_cols, default=numeric_cols[:min(3, len(numeric_cols))])
157
+
158
+ if len(selected_cols) > 0:
159
+ # Buat histogram dengan multiple variables
160
+ fig = go.Figure()
161
+
162
+ for col in selected_cols:
163
+ # Pastikan tidak ada nilai NaN atau infinite
164
+ clean_data = df[col].replace([np.inf, -np.inf], np.nan).dropna()
165
+
166
+ if len(clean_data) > 0:
167
+ fig.add_trace(go.Histogram(
168
+ x=clean_data,
169
+ name=col,
170
+ opacity=0.75
171
+ ))
172
+
173
+ fig.update_layout(
174
+ barmode='overlay',
175
+ title_text='Distribusi Beberapa Variabel',
176
+ xaxis_title_text='Nilai',
177
+ yaxis_title_text='Frekuensi'
178
+ )
179
+
180
+ # Atur opacity untuk melihat overlap
181
+ fig.update_traces(opacity=0.75)
182
+ else:
183
+ st.error("Pilih setidaknya satu kolom untuk histogram")
184
+ return
185
+
186
+ elif chart_type == "Pie Chart":
187
+ # Untuk pie chart, kita butuh kolom kategori dan nilai
188
+ cat_col = st.sidebar.selectbox("Pilih kolom kategori", non_numeric_cols)
189
+ value_col = st.sidebar.selectbox("Pilih kolom nilai", numeric_cols)
190
+
191
+ # Opsi untuk menggabungkan kategori kecil
192
+ combine_threshold = st.sidebar.slider(
193
+ "Gabungkan kategori dengan persentase di bawah (%)",
194
+ min_value=0, max_value=20, value=5, step=1
195
+ )
196
+
197
+ # Hitung total untuk setiap kategori
198
+ pie_data = df.groupby(cat_col)[value_col].sum().reset_index()
199
+ total = pie_data[value_col].sum()
200
+ pie_data['percentage'] = (pie_data[value_col] / total) * 100
201
+
202
+ # Gabungkan kategori kecil
203
+ if combine_threshold > 0:
204
+ other_data = pie_data[pie_data['percentage'] < combine_threshold]
205
+ main_data = pie_data[pie_data['percentage'] >= combine_threshold]
206
+
207
+ if len(other_data) > 0:
208
+ other_sum = other_data[value_col].sum()
209
+ other_percentage = other_data['percentage'].sum()
210
+
211
+ # Buat dataframe baru dengan kategori "Lainnya"
212
+ main_data = pd.concat([
213
+ main_data,
214
+ pd.DataFrame({cat_col: ['Lainnya'], value_col: [other_sum], 'percentage': [other_percentage]})
215
+ ], ignore_index=True)
216
+
217
+ pie_data = main_data
218
+
219
+ fig = px.pie(pie_data, names=cat_col, values=value_col, title=f"Proporsi {value_col} oleh {cat_col}")
220
+
221
+ elif chart_type == "Box Plot":
222
+ col = st.sidebar.selectbox("Pilih kolom", numeric_cols)
223
+ fig = px.box(df, y=col, title=f"Box Plot {col}")
224
+
225
+ elif chart_type == "Heatmap":
226
+ if len(numeric_cols) < 2:
227
+ st.error("Heatmap memerlukan setidaknya 2 kolom numerik")
228
+ return
229
+ selected_cols = st.sidebar.multiselect("Pilih kolom untuk heatmap", numeric_cols, default=numeric_cols[:min(5, len(numeric_cols))])
230
+ if len(selected_cols) < 2:
231
+ st.error("Pilih setidaknya 2 kolom untuk heatmap")
232
+ return
233
+
234
+ # Pastikan tidak ada nilai NaN atau infinite
235
+ clean_df = df[selected_cols].replace([np.inf, -np.inf], np.nan).dropna()
236
+
237
+ if len(clean_df) < 2:
238
+ st.error("Tidak cukup data setelah pembersihan nilai NaN/infinite")
239
+ return
240
+
241
+ corr_matrix = clean_df.corr()
242
+ fig = px.imshow(corr_matrix, text_auto=True, aspect="auto", title="Heatmap Korelasi")
243
+
244
+ elif chart_type == "Candlestick":
245
+ # Untuk candlestick, kita butuh kolom OHLC
246
+ st.sidebar.info("Candlestick memerlukan kolom Open, High, Low, Close")
247
+ date_col = st.sidebar.selectbox("Pilih kolom tanggal", non_numeric_cols)
248
+ open_col = st.sidebar.selectbox("Pilih kolom Open", numeric_cols)
249
+ high_col = st.sidebar.selectbox("Pilih kolom High", numeric_cols)
250
+ low_col = st.sidebar.selectbox("Pilih kolom Low", numeric_cols)
251
+ close_col = st.sidebar.selectbox("Pilih kolom Close", numeric_cols)
252
+
253
+ # Pastikan kolom tanggal dalam format datetime
254
+ try:
255
+ df[date_col] = pd.to_datetime(df[date_col])
256
+ df_sorted = df.sort_values(by=date_col).copy()
257
+
258
+ fig = go.Figure(data=[go.Candlestick(
259
+ x=df_sorted[date_col],
260
+ open=df_sorted[open_col],
261
+ high=df_sorted[high_col],
262
+ low=df_sorted[low_col],
263
+ close=df_sorted[close_col],
264
+ name='Harga Saham'
265
+ )])
266
+
267
+ fig.update_layout(
268
+ title='Chart Candlestick',
269
+ yaxis_title='Harga',
270
+ xaxis_title='Tanggal'
271
+ )
272
+ except Exception as e:
273
+ st.error(f"Error membuat candlestick chart: {str(e)}")
274
+ return
275
+
276
+ # Tampilkan chart
277
+ st.plotly_chart(fig, use_container_width=True)
278
+
279
+ # Tampilkan beberapa chart otomatis berdasarkan data
280
+ st.subheader("Visualisasi Otomatis")
281
+
282
+ # Pilih beberapa kolom numerik untuk ditampilkan
283
+ if len(numeric_cols) > 0:
284
+ selected_cols = st.multiselect(
285
+ "Pilih kolom untuk divisualisasikan",
286
+ numeric_cols,
287
+ default=numeric_cols[:min(3, len(numeric_cols))]
288
+ )
289
+
290
+ if selected_cols:
291
+ col1, col2 = st.columns(2)
292
+
293
+ with col1:
294
+ # Line chart untuk kolom terpilih
295
+ # Pastikan tidak ada nilai NaN atau infinite
296
+ clean_df = df[selected_cols].replace([np.inf, -np.inf], np.nan).dropna()
297
+ fig_multi = px.line(clean_df, y=selected_cols, title="Trend Data")
298
+ st.plotly_chart(fig_multi, use_container_width=True)
299
+
300
+ with col2:
301
+ # Correlation heatmap
302
+ if len(selected_cols) > 1:
303
+ # Pastikan tidak ada nilai NaN atau infinite
304
+ clean_df = df[selected_cols].replace([np.inf, -np.inf], np.nan).dropna()
305
+
306
+ if len(clean_df) > 1:
307
+ corr_matrix = clean_df.corr()
308
+ fig_corr = px.imshow(corr_matrix, text_auto=True, aspect="auto",
309
+ title="Korelasi antar Variabel")
310
+ st.plotly_chart(fig_corr, use_container_width=True)
311
+ else:
312
+ st.error("Tidak cukup data untuk menghitung korelasi setelah pembersihan")
313
+ else:
314
+ # Jika hanya satu kolom, tampilkan box plot
315
+ clean_data = df[selected_cols[0]].replace([np.inf, -np.inf], np.nan).dropna()
316
+ if len(clean_data) > 0:
317
+ fig_box = px.box(y=clean_data, title=f"Distribusi {selected_cols[0]}")
318
+ st.plotly_chart(fig_box, use_container_width=True)
319
+ else:
320
+ st.error("Tidak ada data yang valid untuk ditampilkan")
321
+
322
+ # Fungsi untuk menampilkan statistik deskriptif lengkap
323
+ def show_statistics(df):
324
+ st.header("Statistik Deskriptif")
325
+
326
+ # Tampilkan statistik dasar
327
+ col1, col2, col3, col4 = st.columns(4)
328
+
329
+ with col1:
330
+ st.metric("Jumlah Baris", df.shape[0])
331
+
332
+ with col2:
333
+ st.metric("Jumlah Kolom", df.shape[1])
334
+
335
+ with col3:
336
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
337
+ st.metric("Kolom Numerik", len(numeric_cols))
338
+
339
+ with col4:
340
+ non_numeric_cols = df.select_dtypes(exclude=[np.number]).columns.tolist()
341
+ st.metric("Kolom Non-Numerik", len(non_numeric_cols))
342
+
343
+ # Tampilkan preview data menggunakan HTML
344
+ st.subheader("Preview Data")
345
+ display_dataframe(df)
346
+
347
+ # Tampilkan statistik deskriptif untuk kolom numerik
348
+ if numeric_cols:
349
+ st.subheader("Statistik Numerik Lengkap")
350
+
351
+ # Bersihkan data dari nilai infinite
352
+ clean_df = df[numeric_cols].replace([np.inf, -np.inf], np.nan)
353
+
354
+ # Hitung berbagai statistik
355
+ desc_stats = clean_df.describe()
356
+
357
+ # Tambahkan statistik tambahan
358
+ additional_stats = pd.DataFrame({
359
+ 'median': clean_df.median(),
360
+ 'variance': clean_df.var(),
361
+ 'skewness': clean_df.skew(),
362
+ 'kurtosis': clean_df.kurtosis(),
363
+ 'range': clean_df.max() - clean_df.min(),
364
+ 'Q1': clean_df.quantile(0.25),
365
+ 'Q3': clean_df.quantile(0.75),
366
+ 'IQR': clean_df.quantile(0.75) - clean_df.quantile(0.25)
367
+ }).T
368
+
369
+ # Gabungkan dengan statistik deskriptif standar
370
+ full_stats = pd.concat([desc_stats, additional_stats])
371
+ st.markdown(full_stats.to_html(classes='data-table', escape=False), unsafe_allow_html=True)
372
+
373
+ # Uji normalitas untuk setiap kolom numerik
374
+ st.subheader("Uji Normalitas (Shapiro-Wilk)")
375
+ normality_results = []
376
+ for col in numeric_cols:
377
+ # Bersihkan data dari nilai infinite dan NaN
378
+ data = df[col].replace([np.inf, -np.inf], np.nan).dropna()
379
+ if len(data) > 3 and len(data) < 5000: # Shapiro-Wilk bekerja untuk 3 < n < 5000
380
+ try:
381
+ stat, p_value = stats.shapiro(data)
382
+ normality_results.append({
383
+ 'Kolom': col,
384
+ 'Statistik': f"{stat:.4f}",
385
+ 'p-value': f"{p_value:.4f}",
386
+ 'Normal': "Ya" if p_value > 0.05 else "Tidak"
387
+ })
388
+ except Exception as e:
389
+ normality_results.append({
390
+ 'Kolom': col,
391
+ 'Statistik': f"Error: {str(e)}",
392
+ 'p-value': "Error",
393
+ 'Normal': "Error"
394
+ })
395
+ else:
396
+ normality_results.append({
397
+ 'Kolom': col,
398
+ 'Statistik': "N/A",
399
+ 'p-value': "N/A",
400
+ 'Normal': "Sample size tidak sesuai"
401
+ })
402
+
403
+ normality_df = pd.DataFrame(normality_results)
404
+ st.markdown(normality_df.to_html(classes='data-table', escape=False, index=False), unsafe_allow_html=True)
405
+
406
+ # Tampilkan informasi tentang missing values
407
+ st.subheader("Informasi Missing Values")
408
+ missing_data = df.isnull().sum()
409
+ missing_df = pd.DataFrame({
410
+ 'Kolom': missing_data.index,
411
+ 'Jumlah Missing': missing_data.values,
412
+ 'Persentase Missing': (missing_data.values / len(df)) * 100
413
+ })
414
+ st.markdown(missing_df.to_html(classes='data-table', escape=False, index=False), unsafe_allow_html=True)
415
+
416
+ # Tampilkan informasi tentang infinite values
417
+ st.subheader("Informasi Infinite Values")
418
+ if numeric_cols:
419
+ inf_data = {}
420
+ for col in numeric_cols:
421
+ inf_count = np.isinf(df[col]).sum()
422
+ inf_data[col] = inf_count
423
+
424
+ inf_df = pd.DataFrame({
425
+ 'Kolom': list(inf_data.keys()),
426
+ 'Jumlah Infinite': list(inf_data.values()),
427
+ 'Persentase Infinite': [f"{(v / len(df)) * 100:.2f}%" for v in inf_data.values()]
428
+ })
429
+ st.markdown(inf_df.to_html(classes='data-table', escape=False, index=False), unsafe_allow_html=True)
430
+
431
+ # Tampilkan informasi tipe data
432
+ st.subheader("Informasi Tipe Data")
433
+ dtype_info = pd.DataFrame({
434
+ 'Kolom': df.columns,
435
+ 'Tipe Data': df.dtypes.values,
436
+ 'Nilai Unik': [df[col].nunique() for col in df.columns]
437
+ })
438
+ st.markdown(dtype_info.to_html(classes='data-table', escape=False, index=False), unsafe_allow_html=True)
439
+
440
+ # Analisis trend saham jika ada kolom yang sesuai
441
+ analyze_stock_trends(df)
442
+
443
+ # Fungsi untuk menganalisis trend saham naik/turun
444
+ def analyze_stock_trends(df):
445
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
446
+ date_cols = df.select_dtypes(include=['datetime64']).columns.tolist()
447
+
448
+ if not date_cols:
449
+ # Coba konversi kolom string ke datetime
450
+ for col in df.columns:
451
+ if df[col].dtype == 'object':
452
+ try:
453
+ df[col] = pd.to_datetime(df[col])
454
+ date_cols.append(col)
455
+ break
456
+ except:
457
+ pass
458
+
459
+ if date_cols and numeric_cols:
460
+ st.subheader("Analisis Trend Saham")
461
+
462
+ date_col = st.selectbox("Pilih kolom tanggal", date_cols)
463
+ price_col = st.selectbox("Pilih kolom harga", numeric_cols)
464
+
465
+ try:
466
+ # Pastikan data diurutkan berdasarkan tanggal
467
+ df_sorted = df.sort_values(by=date_col).copy()
468
+
469
+ # Bersihkan data dari nilai infinite
470
+ df_sorted[price_col] = df_sorted[price_col].replace([np.inf, -np.inf], np.nan)
471
+ df_sorted = df_sorted.dropna(subset=[price_col])
472
+
473
+ # Hitung perubahan harga
474
+ df_sorted['Perubahan'] = df_sorted[price_col].pct_change() * 100
475
+ df_sorted['Perubahan_Abs'] = df_sorted[price_col].diff()
476
+
477
+ # Hitung statistik trend
478
+ total_days = len(df_sorted)
479
+ up_days = len(df_sorted[df_sorted['Perubahan_Abs'] > 0])
480
+ down_days = len(df_sorted[df_sorted['Perubahan_Abs'] < 0])
481
+ flat_days = len(df_sorted[df_sorted['Perubahan_Abs'] == 0])
482
+
483
+ avg_change = df_sorted['Perubahan'].mean()
484
+ max_gain = df_sorted['Perubahan'].max()
485
+ max_loss = df_sorted['Perubahan'].min()
486
+
487
+ # Tampilkan metrik trend
488
+ col1, col2, col3, col4 = st.columns(4)
489
+
490
+ with col1:
491
+ trend_icon = "📈" if avg_change > 0 else "📉"
492
+ st.metric(f"Rata-rata Perubahan {trend_icon}", f"{avg_change:.2f}%")
493
+
494
+ with col2:
495
+ st.metric("Hari Naik", f"{up_days} ({up_days/total_days*100:.1f}%)")
496
+
497
+ with col3:
498
+ st.metric("Hari Turun", f"{down_days} ({down_days/total_days*100:.1f}%)")
499
+
500
+ with col4:
501
+ st.metric("Hari Datar", f"{flat_days} ({flat_days/total_days*100:.1f}%)")
502
+
503
+ # Analisis volatilitas
504
+ st.subheader("Analisis Volatilitas")
505
+
506
+ # Hitung volatilitas (standar deviasi dari perubahan harga)
507
+ volatility = df_sorted['Perubahan'].std()
508
+
509
+ # Hitung moving averages
510
+ df_sorted['MA_7'] = df_sorted[price_col].rolling(window=7).mean()
511
+ df_sorted['MA_20'] = df_sorted[price_col].rolling(window=20).mean()
512
+
513
+ # Hitung RSI (Relative Strength Index)
514
+ delta = df_sorted[price_col].diff()
515
+ gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
516
+ loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
517
+ rs = gain / loss
518
+ df_sorted['RSI'] = 100 - (100 / (1 + rs))
519
+
520
+ col1, col2, col3 = st.columns(3)
521
+
522
+ with col1:
523
+ st.metric("Volatilitas (Std Dev)", f"{volatility:.2f}%")
524
+
525
+ with col2:
526
+ # Tentukan apakah dalam kondisi overbought atau oversold berdasarkan RSI
527
+ if 'RSI' in df_sorted.columns and not df_sorted['RSI'].isna().all():
528
+ latest_rsi = df_sorted['RSI'].iloc[-1]
529
+ rsi_status = "Overbought (>70)" if latest_rsi > 70 else "Oversold (<30)" if latest_rsi < 30 else "Netral"
530
+ st.metric("RSI Terakhir", f"{latest_rsi:.2f}", rsi_status)
531
+
532
+ with col3:
533
+ # Tentukan trend berdasarkan moving averages
534
+ if 'MA_7' in df_sorted.columns and 'MA_20' in df_sorted.columns:
535
+ latest_ma7 = df_sorted['MA_7'].iloc[-1]
536
+ latest_ma20 = df_sorted['MA_20'].iloc[-1]
537
+ ma_trend = "Uptrend" if latest_ma7 > latest_ma20 else "Downtrend"
538
+ st.metric("Trend MA", ma_trend)
539
+
540
+ # Tampilkan grafik harga dengan matplotlib
541
+ st.subheader("Grafik Harga dengan Analisis Teknikal")
542
+
543
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10))
544
+
545
+ # Plot harga dan moving averages
546
+ ax1.plot(df_sorted[date_col], df_sorted[price_col], label='Harga', color='blue', linewidth=1)
547
+ if 'MA_7' in df_sorted.columns:
548
+ ax1.plot(df_sorted[date_col], df_sorted['MA_7'], label='MA 7', color='orange', linewidth=1)
549
+ if 'MA_20' in df_sorted.columns:
550
+ ax1.plot(df_sorted[date_col], df_sorted['MA_20'], label='MA 20', color='red', linewidth=1)
551
+ ax1.set_ylabel('Harga')
552
+ ax1.set_title('Harga dan Moving Averages')
553
+ ax1.grid(True)
554
+ ax1.legend()
555
+
556
+ # Format tanggal pada sumbu x
557
+ ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
558
+ ax1.xaxis.set_major_locator(mdates.MonthLocator())
559
+ plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
560
+
561
+ # Plot perubahan harga
562
+ colors = ['green' if x >= 0 else 'red' for x in df_sorted['Perubahan_Abs']]
563
+ ax2.bar(df_sorted[date_col], df_sorted['Perubahan_Abs'], color=colors, alpha=0.7)
564
+ ax2.set_ylabel('Perubahan')
565
+ ax2.set_title('Perubahan Harga Harian')
566
+ ax2.grid(True)
567
+
568
+ # Format tanggal pada sumbu x
569
+ ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
570
+ ax2.xaxis.set_major_locator(mdates.MonthLocator())
571
+ plt.setp(ax2.xaxis.get_majorticklabels(), rotation=45)
572
+
573
+ # Plot RSI jika tersedia
574
+ if 'RSI' in df_sorted.columns:
575
+ ax3.plot(df_sorted[date_col], df_sorted['RSI'], label='RSI', color='purple', linewidth=1)
576
+ ax3.axhline(y=70, color='r', linestyle='--', alpha=0.7, label='Overbought (70)')
577
+ ax3.axhline(y=30, color='g', linestyle='--', alpha=0.7, label='Oversold (30)')
578
+ ax3.set_ylabel('RSI')
579
+ ax3.set_title('Relative Strength Index (RSI)')
580
+ ax3.grid(True)
581
+ ax3.legend()
582
+
583
+ # Format tanggal pada sumbu x
584
+ ax3.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
585
+ ax3.xaxis.set_major_locator(mdates.MonthLocator())
586
+ plt.setp(ax3.xaxis.get_majorticklabels(), rotation=45)
587
+
588
+ plt.tight_layout()
589
+ st.pyplot(fig)
590
+
591
+ # Tampilkan sinyal trading sederhana
592
+ st.subheader("Sinyal Trading Sederhana")
593
+
594
+ if 'RSI' in df_sorted.columns and 'MA_7' in df_sorted.columns and 'MA_20' in df_sorted.columns:
595
+ # Ambil data terbaru
596
+ latest_data = df_sorted.iloc[-1]
597
+
598
+ # Generate sinyal berdasarkan RSI dan MA
599
+ signals = []
600
+
601
+ if latest_data['RSI'] < 30 and latest_data['MA_7'] > latest_data['MA_20']:
602
+ signals.append(("BUY", "RSI oversold dan MA7 > MA20"))
603
+ elif latest_data['RSI'] > 70 and latest_data['MA_7'] < latest_data['MA_20']:
604
+ signals.append(("SELL", "RSI overbought dan MA7 < MA20"))
605
+ elif latest_data['RSI'] < 30:
606
+ signals.append(("POTENTIAL BUY", "RSI oversold"))
607
+ elif latest_data['RSI'] > 70:
608
+ signals.append(("POTENTIAL SELL", "RSI overbought"))
609
+ elif latest_data['MA_7'] > latest_data['MA_20']:
610
+ signals.append(("BULLISH", "MA7 > MA20 (Uptrend)"))
611
+ elif latest_data['MA_7'] < latest_data['MA_20']:
612
+ signals.append(("BEARISH", "MA7 < MA20 (Downtrend)"))
613
+ else:
614
+ signals.append(("NEUTRAL", "Tidak ada sinyal kuat"))
615
+
616
+ # Tampilkan sinyal
617
+ for signal, reason in signals:
618
+ if signal == "BUY":
619
+ st.success(f"🚀 {signal}: {reason}")
620
+ elif signal == "SELL":
621
+ st.error(f"🔻 {signal}: {reason}")
622
+ elif signal in ["POTENTIAL BUY", "BULLISH"]:
623
+ st.info(f"📈 {signal}: {reason}")
624
+ elif signal in ["POTENTIAL SELL", "BEARISH"]:
625
+ st.warning(f"📉 {signal}: {reason}")
626
+ else:
627
+ st.info(f"📊 {signal}: {reason}")
628
+
629
+ except Exception as e:
630
+ st.error(f"Error dalam analisis trend: {str(e)}")
631
+
632
+ # Fungsi untuk membuat file contoh dengan data saham
633
+ def create_sample_file():
634
+ dates = pd.date_range(start='2023-01-01', periods=100, freq='D')
635
+
636
+ # Generate harga saham dengan random walk
637
+ np.random.seed(42)
638
+ price_changes = np.random.normal(0.001, 0.02, len(dates))
639
+ prices = 100 * (1 + price_changes).cumprod()
640
+
641
+ # Generate volume
642
+ volumes = np.random.randint(1000, 10000, len(dates))
643
+
644
+ example_data = pd.DataFrame({
645
+ 'Tanggal': dates,
646
+ 'Open': prices * (1 + np.random.normal(0, 0.01, len(dates))),
647
+ 'High': prices * (1 + np.abs(np.random.normal(0.005, 0.01, len(dates)))),
648
+ 'Low': prices * (1 - np.abs(np.random.normal(0.005, 0.01, len(dates)))),
649
+ 'Close': prices,
650
+ 'Volume': volumes,
651
+ 'Perusahaan': np.random.choice(['AAPL', 'GOOGL', 'MSFT', 'AMZN'], len(dates)),
652
+ 'Sektor': np.random.choice(['Teknologi', 'Kesehatan', 'Finansial', 'Konsumsi'], len(dates))
653
+ })
654
+
655
+ # Adjust OHLC untuk konsistensi
656
+ for i in range(len(example_data)):
657
+ example_data.loc[i, 'High'] = max(example_data.loc[i, 'Open'], example_data.loc[i, 'Close'], example_data.loc[i, 'High'])
658
+ example_data.loc[i, 'Low'] = min(example_data.loc[i, 'Open'], example_data.loc[i, 'Close'], example_data.loc[i, 'Low'])
659
+
660
+ return example_data
661
+
662
+ # Fungsi untuk analisis korelasi antar saham
663
+ def analyze_stock_correlation(df):
664
+ st.header("Analisis Korelasi Antar Saham")
665
+
666
+ # Identifikasi kolom yang mungkin berisi data harga saham
667
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
668
+ date_cols = df.select_dtypes(include=['datetime64']).columns.tolist()
669
+
670
+ if not date_cols:
671
+ for col in df.columns:
672
+ if df[col].dtype == 'object':
673
+ try:
674
+ df[col] = pd.to_datetime(df[col])
675
+ date_cols.append(col)
676
+ break
677
+ except:
678
+ pass
679
+
680
+ if date_cols and len(numeric_cols) > 1:
681
+ date_col = st.selectbox("Pilih kolom tanggal untuk analisis korelasi", date_cols)
682
+ price_cols = st.multiselect("Pilih kolom harga untuk analisis korelasi", numeric_cols, default=numeric_cols[:min(5, len(numeric_cols))])
683
+
684
+ if len(price_cols) > 1:
685
+ try:
686
+ # Pastikan data diurutkan berdasarkan tanggal
687
+ df_sorted = df.sort_values(by=date_col).copy()
688
+
689
+ # Bersihkan data dari nilai infinite dan NaN
690
+ clean_data = df_sorted[price_cols].replace([np.inf, -np.inf], np.nan).dropna()
691
+
692
+ if len(clean_data) > 1:
693
+ # Hitung korelasi
694
+ correlation_matrix = clean_data.corr()
695
+
696
+ # Tampilkan heatmap korelasi
697
+ fig = px.imshow(correlation_matrix,
698
+ text_auto=True,
699
+ aspect="auto",
700
+ title="Korelasi Antar Saham",
701
+ color_continuous_scale='RdBu_r',
702
+ zmin=-1, zmax=1)
703
+ st.plotly_chart(fig, use_container_width=True)
704
+
705
+ # Tampilkan matriks korelasi sebagai tabel
706
+ st.subheader("Matriks Korelasi")
707
+ st.dataframe(correlation_matrix.style.background_gradient(cmap='RdBu_r', vmin=-1, vmax=1).format("{:.2f}"))
708
+
709
+ # Analisis pasangan dengan korelasi tertinggi dan terendah
710
+ st.subheader("Pasangan dengan Korelasi Ekstrem")
711
+
712
+ # Dapatkan pasangan dengan korelasi tertinggi dan terendah
713
+ corr_pairs = []
714
+ for i in range(len(correlation_matrix.columns)):
715
+ for j in range(i+1, len(correlation_matrix.columns)):
716
+ corr_pairs.append({
717
+ 'Pair': f"{correlation_matrix.columns[i]} - {correlation_matrix.columns[j]}",
718
+ 'Correlation': correlation_matrix.iloc[i, j]
719
+ })
720
+
721
+ corr_df = pd.DataFrame(corr_pairs)
722
+ top_corr = corr_df.nlargest(3, 'Correlation')
723
+ bottom_corr = corr_df.nsmallest(3, 'Correlation')
724
+
725
+ col1, col2 = st.columns(2)
726
+
727
+ with col1:
728
+ st.markdown("**Korelasi Tertinggi**")
729
+ for _, row in top_corr.iterrows():
730
+ st.write(f"{row['Pair']}: {row['Correlation']:.3f}")
731
+
732
+ with col2:
733
+ st.markdown("**Korelasi Terendah**")
734
+ for _, row in bottom_corr.iterrows():
735
+ st.write(f"{row['Pair']}: {row['Correlation']:.3f}")
736
+
737
+ else:
738
+ st.error("Tidak cukup data untuk analisis korelasi setelah pembersihan")
739
+ except Exception as e:
740
+ st.error(f"Error dalam analisis korelasi: {str(e)}")
741
+ else:
742
+ st.error("Pilih setidaknya 2 kolom harga untuk analisis korelasi")
743
+ else:
744
+ st.error("Data tidak memiliki cukup kolom tanggal atau numerik untuk analisis korelasi")
745
+
746
+ # UI utama
747
+ st.markdown('<h1 class="main-header">📊 Dashboard Statistik Profesional</h1>', unsafe_allow_html=True)
748
+ st.markdown("Unggah file CSV atau Excel untuk melihat visualisasi dan statistik data.")
749
+
750
+ # Sidebar untuk upload file dan kontrol
751
+ st.sidebar.header("Kontrol Aplikasi")
752
+
753
+ # Tombol untuk membuat dan mengunduh file contoh
754
+ if st.sidebar.button("Buat File Contoh"):
755
+ example_data = create_sample_file()
756
+ csv = example_data.to_csv(index=False)
757
+ st.sidebar.download_button(
758
+ label="Unduh Contoh CSV",
759
+ data=csv,
760
+ file_name="contoh_data_saham.csv",
761
+ mime="text/csv"
762
+ )
763
+
764
+ # Fitur untuk mengunggah dan menggabungkan beberapa file
765
+ st.sidebar.header("Unggah & Gabungkan Beberapa File")
766
+ uploaded_files = st.sidebar.file_uploader(
767
+ "Pilih file CSV atau Excel (bisa multiple)",
768
+ type=['csv', 'xlsx', 'xls'],
769
+ help="Format yang didukung: CSV, XLSX, XLS",
770
+ accept_multiple_files=True
771
+ )
772
+
773
+ # Pilihan metode penggabungan jika ada beberapa file
774
+ merge_method = "concat"
775
+ if uploaded_files and len(uploaded_files) > 1:
776
+ merge_method = st.sidebar.selectbox(
777
+ "Metode Penggabungan Data",
778
+ ["concat", "inner", "outer", "left", "right"],
779
+ help="Concat: menggabungkan baris, lainnya: menggabungkan berdasarkan kolom"
780
+ )
781
+
782
+ # Proses file yang diupload
783
+ df = None
784
+ if uploaded_files:
785
+ datasets = []
786
+ for uploaded_file in uploaded_files:
787
+ dataset = process_uploaded_file(uploaded_file)
788
+ if dataset is not None:
789
+ datasets.append(dataset)
790
+ st.sidebar.success(f"File berhasil diunggah: {uploaded_file.name}")
791
+
792
+ if datasets:
793
+ if len(datasets) == 1:
794
+ df = datasets[0]
795
+ else:
796
+ df = merge_datasets(datasets, merge_method)
797
+ st.sidebar.success(f"Berhasil menggabungkan {len(datasets)} dataset")
798
+
799
+ # Jika file sudah diupload
800
+ if df is not None:
801
+ # Tampilkan statistik
802
+ show_statistics(df)
803
+
804
+ # Tampilkan visualisasi
805
+ st.header("Visualisasi Data")
806
+ create_visualizations(df)
807
+
808
+ # Analisis korelasi antar saham
809
+ analyze_stock_correlation(df)
810
+
811
+ # Tambahkan opsi untuk mengunduh data yang telah diproses
812
+ st.sidebar.header("Unduh Data")
813
+ if st.sidebar.button("Unduh Data yang Telah Diproses"):
814
+ csv = df.to_csv(index=False)
815
+ st.sidebar.download_button(
816
+ label="Unduh sebagai CSV",
817
+ data=csv,
818
+ file_name="processed_data.csv",
819
+ mime="text/csv"
820
+ )
821
+
822
+ else:
823
+ # Tampilkan contoh data jika belum ada file yang diupload
824
+ st.info("Silakan unggah file CSV atau Excel melalui sidebar di sebelah kiri.")
825
+
826
+ # Buat contoh data
827
+ st.subheader("Contoh Data")
828
+ example_data = create_sample_file()
829
+ display_dataframe(example_data)
830
+
831
+ # Tampilkan contoh visualisasi
832
+ st.subheader("Contoh Visualisasi")
833
+
834
+ col1, col2 = st.columns(2)
835
+
836
+ with col1:
837
+ fig_example = px.line(example_data, x='Tanggal', y='Close', title='Contoh Trend Harga Saham')
838
+ st.plotly_chart(fig_example, use_container_width=True)
839
+
840
+ with col2:
841
+ # Pie chart contoh
842
+ sector_data = example_data.groupby('Sektor')['Volume'].sum().reset_index()
843
+ fig_pie = px.pie(sector_data, names='Sektor', values='Volume', title='Volume Perdagangan per Sektor')
844
+ st.plotly_chart(fig_pie, use_container_width=True)
845
+
846
+ # Footer
847
+ st.markdown("---")
848
+ st.markdown("Dashboard Statistik © 2025 - Dibuat oleh Dwi Bakti N Dev")