Spaces:
Sleeping
Sleeping
| # ============================================================================= | |
| # リスクモニタリングシステム プロトタイプ | |
| # Risk Monitoring Dashboard - Shiny App | |
| # ============================================================================= | |
| library(shiny) | |
| library(bslib) | |
| # library(leaflet) # Replaced with plotly geo | |
| library(plotly) | |
| library(DT) | |
| library(dplyr) | |
| library(lubridate) | |
| # ============================================================================= | |
| # ダミーデータの生成 | |
| # ============================================================================= | |
| # 国別リスクデータ | |
| country_data <- data.frame( | |
| country = c("Venezuela", "Iran", "Syria", "Yemen", "Afghanistan", | |
| "Myanmar", "North Korea", "Libya", "Somalia", "Sudan", | |
| "Japan", "USA", "Germany", "France", "UK", | |
| "Brazil", "India", "China", "Russia", "South Africa"), | |
| country_jp = c("ベネズエラ", "イラン", "シリア", "イエメン", "アフガニスタン", | |
| "ミャンマー", "北朝鮮", "リビア", "ソマリア", "スーダン", | |
| "日本", "アメリカ", "ドイツ", "フランス", "イギリス", | |
| "ブラジル", "インド", "中国", "ロシア", "南アフリカ"), | |
| risk_score = c(95, 92, 88, 85, 82, 78, 76, 72, 70, 68, | |
| 15, 22, 18, 20, 19, 45, 38, 52, 65, 48), | |
| lat = c(6.4238, 32.4279, 34.8021, 15.5527, 33.9391, | |
| 21.9162, 40.3399, 26.3351, 5.1521, 12.8628, | |
| 36.2048, 37.0902, 51.1657, 46.2276, 55.3781, | |
| -14.2350, 20.5937, 35.8617, 61.5240, -30.5595), | |
| lng = c(-66.5897, 53.6880, 38.9968, 48.5164, 67.7100, | |
| 95.9560, 127.5101, 17.2283, 46.1996, 30.2176, | |
| 138.2529, -95.7129, 10.4515, 2.2137, -3.4360, | |
| -51.9253, 78.9629, 104.1954, 105.3188, 22.9375), | |
| trend = c("up", "up", "stable", "up", "down", | |
| "up", "stable", "down", "stable", "up", | |
| "stable", "stable", "stable", "stable", "stable", | |
| "up", "stable", "up", "up", "stable"), | |
| region = c("South America", "Middle East", "Middle East", "Middle East", "Central Asia", | |
| "Southeast Asia", "East Asia", "Africa", "Africa", "Africa", | |
| "East Asia", "North America", "Europe", "Europe", "Europe", | |
| "South America", "South Asia", "East Asia", "Europe", "Africa"), | |
| stringsAsFactors = FALSE | |
| ) | |
| # 時系列リスクスコアデータ生成 | |
| generate_time_series <- function(country, base_score) { | |
| dates <- seq(as.Date("2024-07-01"), as.Date("2025-01-11"), by = "day") | |
| n <- length(dates) | |
| noise <- cumsum(rnorm(n, 0, 1.5)) | |
| scores <- pmax(0, pmin(100, base_score + noise - mean(noise))) | |
| # 最近のスパイクを追加(高リスク国の場合) | |
| if (base_score > 70) { | |
| spike_start <- n - 14 | |
| scores[spike_start:n] <- scores[spike_start:n] + seq(0, 15, length.out = 15) | |
| scores <- pmin(100, scores) | |
| } | |
| data.frame( | |
| date = dates, | |
| score = round(scores, 1), | |
| country = country | |
| ) | |
| } | |
| # ニュースデータ | |
| news_data <- data.frame( | |
| id = 1:20, | |
| country = c("Venezuela", "Venezuela", "Iran", "Iran", "Syria", | |
| "Myanmar", "Brazil", "China", "Russia", "Japan", | |
| "Venezuela", "Iran", "Syria", "Yemen", "Afghanistan", | |
| "North Korea", "Libya", "India", "Germany", "USA"), | |
| date = c("2025-01-11", "2025-01-10", "2025-01-11", "2025-01-09", "2025-01-08", | |
| "2025-01-11", "2025-01-10", "2025-01-09", "2025-01-08", "2025-01-07", | |
| "2025-01-06", "2025-01-05", "2025-01-04", "2025-01-03", "2025-01-02", | |
| "2025-01-01", "2024-12-31", "2024-12-30", "2024-12-29", "2024-12-28"), | |
| title = c( | |
| "Political Unrest Reported in Venezuela: Opposition Rally", | |
| "Economic Crisis Deepens as Currency Devaluation Continues", | |
| "Nuclear Negotiations Stall Amid Rising Tensions", | |
| "New Sanctions Announced by Western Nations", | |
| "Humanitarian Situation Worsens in Northern Regions", | |
| "Military Junta Tightens Control Over Media", | |
| "Currency Volatility Raises Investor Concerns", | |
| "US-China Trade Tensions Escalate Over Tariffs", | |
| "Energy Export Restrictions Create Market Uncertainty", | |
| "Earthquake Preparedness Measures Enhanced", | |
| "Mass Protests Enter Second Week in Caracas", | |
| "Oil Production Disrupted by Infrastructure Issues", | |
| "Refugee Crisis Intensifies at Border Regions", | |
| "Ceasefire Negotiations Show Signs of Progress", | |
| "Security Concerns Rise After Attack on Aid Workers", | |
| "Missile Test Condemned by International Community", | |
| "Political Transition Faces New Challenges", | |
| "Economic Growth Projections Revised Downward", | |
| "Coalition Government Navigates Policy Disputes", | |
| "Federal Reserve Signals Cautious Approach" | |
| ), | |
| category = c("政変の予兆", "経済危機", "地政学リスク", "制裁", "人道危機", | |
| "政変の予兆", "経済危機", "貿易摩擦", "エネルギー", "自然災害", | |
| "政変の予兆", "経済危機", "人道危機", "紛争", "テロ関連", | |
| "地政学リスク", "政変の予兆", "経済危機", "政治", "経済政策"), | |
| what = c( | |
| "ベネズエラで大規模な反政府デモが発生。野党支持者数万人がカラカス中心部に集結。", | |
| "ボリバル通貨が対ドルで過去最安値を更新。インフレ率が年率200%を超える見通し。", | |
| "イランと西側諸国の核協議が決裂。両者の立場の隔たりが鮮明に。", | |
| "EUがイランに対する追加制裁を発表。金融セクターが主な対象。", | |
| "北部地域で食料・医療品の不足が深刻化。国連が緊急支援を要請。", | |
| "軍事政権がソーシャルメディアへのアクセスを制限。報道の自由に懸念。", | |
| "レアルが週間で8%下落。中央銀行が市場介入を実施。", | |
| "米国が中国製品への追加関税を発表。中国側も報復措置を示唆。", | |
| "ロシアがガス輸出制限を示唆。欧州のエネルギー価格が急騰。", | |
| "南海トラフ地震への備えとして、政府が新たな防災計画を発表。", | |
| "カラカスでの抗議活動が2週目に突入。治安部隊との衝突で負傷者。", | |
| "イラン南部の製油所で火災発生。原油生産に影響。", | |
| "シリア北部から数千人規模の難民がトルコ国境に殺到。", | |
| "イエメン和平交渉で一時停戦に合意。持続性には疑問符。", | |
| "アフガニスタンで援助団体の車列が襲撃される。職員2名が死亡。", | |
| "北朝鮮が短距離弾道ミサイルを発射。日本海に落下。", | |
| "リビア暫定政府と東部勢力の対話が行き詰まり。", | |
| "インドの製造業PMIが予想を下回る。景気減速懸念が強まる。", | |
| "ドイツ連立政権内で気候政策を巡り対立。", | |
| "FRBが金利据え置きを決定。年内の利下げ観測が後退。" | |
| ), | |
| cause = c( | |
| "2024年大統領選挙の結果を巡る対立が激化。野党側は選挙不正を主張し、国際社会からも懸念の声。マドゥロ政権への不満が限界に達しつつある。", | |
| "米国の経済制裁による外貨収入の減少と、政府の財政政策の失敗が重なり、通貨危機が深刻化。国民の購買力が著しく低下。", | |
| "イラン側がウラン濃縮活動の縮小を拒否。西側諸国は制裁解除の条件として核開発の完全な透明性を要求しており、双方の要求が折り合わず。", | |
| "イランの中東地域での軍事活動とミサイル開発を問題視。ドローン供与などがエスカレーションの一因。", | |
| "長期化する内戦と経済制裁の影響で、民間インフラが崩壊。医療システムの機能不全が深刻な人道危機を招く。", | |
| "2021年のクーデター以降、民主化運動の弾圧が継続。国際社会からの孤立が深まる中、情報統制を強化。", | |
| "米国の金利高止まりと中国経済の減速を背景に、新興国通貨全般が下落圧力に。ブラジル固有の財政問題も重荷。", | |
| "半導体やEVを巡る技術覇権争いが激化。両国の経済的相互依存関係が政治的緊張により複雑化。", | |
| "ウクライナ紛争の長期化に伴い、ロシアがエネルギーを外交カードとして使用。欧州のロシア依存度の高さが露呈。", | |
| "2024年の能登半島地震を教訓に、政府が防災体制の見直しを加速。インフラ強靭化に多額の予算を投入予定。", | |
| "選挙結果を認めない野党支持者と、政権維持を図る与党支持者の対立が先鋭化。国際社会の介入を求める声も。", | |
| "老朽化したインフラと投資不足が原因。制裁下でメンテナンス部品の調達が困難になっている実態。", | |
| "北部での軍事衝突再発と、食料危機の深刻化が人々を避難に駆り立てる。トルコは受け入れ能力の限界を表明。", | |
| "国連の仲介努力とサウジアラビアの外交政策転換が合意に寄与。しかし過去の停戦も短期間で崩壊した経緯あり。", | |
| "タリバン政権下での治安悪化と、反政府勢力の活動活発化が背景。外国人・援助関係者が標的にされるケースが増加。", | |
| "米韓合同軍事演習への対抗措置として実施。核・ミサイル開発の進展をアピールする狙いも。", | |
| "東西の主要勢力間の権力闘争と、石油収入の配分を巡る対立が根深い。国際社会の調停努力も限定的効果。", | |
| "世界経済の減速と国内の構造改革の遅れが影響。特にIT・自動車セクターの輸出減少が顕著。", | |
| "緑の党と自由民主党の政策スタンスの違いが表面化。2025年の連邦議会選挙を控え、各党が独自色を強調。", | |
| "インフレ率の高止まりと雇用市場の堅調さを理由に、FRBは慎重姿勢を維持。市場の利下げ期待との乖離。" | |
| ), | |
| impact_score = c(8, 7, 9, 7, 6, 6, 5, 8, 9, 3, | |
| 8, 6, 7, 5, 7, 8, 5, 4, 3, 4), | |
| source = rep(c("Reuters", "Bloomberg", "AP News", "AFP", "BBC"), 4), | |
| stringsAsFactors = FALSE | |
| ) | |
| # ============================================================================= | |
| # UI定義 | |
| # ============================================================================= | |
| ui <- page_navbar( | |
| title = tags$span( | |
| style = "font-weight: 700; font-size: 1.1rem;", | |
| tags$span(style = "color: #FF6B35;", "◆"), | |
| " リスクモニタリングシステム" | |
| ), | |
| id = "navbar", | |
| theme = bs_theme( | |
| version = 5, | |
| bootswatch = "flatly", | |
| primary = "#1A365D", | |
| secondary = "#718096", | |
| success = "#38A169", | |
| danger = "#E53E3E", | |
| warning = "#DD6B20", | |
| info = "#3182CE", | |
| "navbar-bg" = "#1A365D", | |
| "body-bg" = "#F7FAFC", | |
| base_font = font_google("Noto Sans JP"), | |
| heading_font = font_google("Noto Sans JP"), | |
| font_scale = 0.9 | |
| ), | |
| header = tags$head( | |
| tags$style(HTML(" | |
| :root { | |
| --risk-high: #E53E3E; | |
| --risk-medium: #DD6B20; | |
| --risk-low: #38A169; | |
| --accent: #FF6B35; | |
| } | |
| .navbar { | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| .card { | |
| border: none; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| .card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 30px rgba(0,0,0,0.12); | |
| } | |
| .card-header { | |
| background: linear-gradient(135deg, #1A365D 0%, #2D4A7C 100%); | |
| color: white; | |
| font-weight: 600; | |
| border-radius: 12px 12px 0 0 !important; | |
| padding: 1rem 1.25rem; | |
| } | |
| .score-high { color: var(--risk-high); font-weight: 700; } | |
| .score-medium { color: var(--risk-medium); font-weight: 700; } | |
| .score-low { color: var(--risk-low); font-weight: 700; } | |
| .country-list-item { | |
| padding: 12px 16px; | |
| border-bottom: 1px solid #E2E8F0; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .country-list-item:hover { | |
| background: #EDF2F7; | |
| } | |
| .country-list-item.active { | |
| background: linear-gradient(90deg, #FF6B35 0%, #FF8C5A 100%); | |
| color: white; | |
| font-weight: 600; | |
| } | |
| .news-card { | |
| background: white; | |
| border-radius: 8px; | |
| padding: 16px; | |
| margin-bottom: 12px; | |
| border-left: 4px solid var(--accent); | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.06); | |
| } | |
| .news-title { | |
| font-weight: 600; | |
| color: #1A365D; | |
| margin-bottom: 8px; | |
| } | |
| .news-meta { | |
| font-size: 0.8rem; | |
| color: #718096; | |
| } | |
| .category-badge { | |
| display: inline-block; | |
| padding: 4px 10px; | |
| border-radius: 20px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| margin-right: 8px; | |
| } | |
| .category-政変の予兆 { background: #FED7D7; color: #C53030; } | |
| .category-経済危機 { background: #FEEBC8; color: #C05621; } | |
| .category-地政学リスク { background: #E9D8FD; color: #6B46C1; } | |
| .category-テロ関連 { background: #FED7E2; color: #B83280; } | |
| .category-人道危機 { background: #C6F6D5; color: #276749; } | |
| .category-制裁 { background: #BEE3F8; color: #2B6CB0; } | |
| .category-紛争 { background: #FEEBC8; color: #C05621; } | |
| .category-貿易摩擦 { background: #E9D8FD; color: #6B46C1; } | |
| .category-エネルギー { background: #FEFCBF; color: #975A16; } | |
| .category-自然災害 { background: #C6F6D5; color: #276749; } | |
| .category-政治 { background: #BEE3F8; color: #2B6CB0; } | |
| .category-経済政策 { background: #E2E8F0; color: #4A5568; } | |
| .chat-container { | |
| height: 500px; | |
| overflow-y: auto; | |
| padding: 16px; | |
| background: #F7FAFC; | |
| border-radius: 8px; | |
| } | |
| .chat-message { | |
| margin-bottom: 16px; | |
| max-width: 85%; | |
| } | |
| .chat-message.user { | |
| margin-left: auto; | |
| background: linear-gradient(135deg, #1A365D 0%, #2D4A7C 100%); | |
| color: white; | |
| padding: 12px 16px; | |
| border-radius: 16px 16px 4px 16px; | |
| } | |
| .chat-message.assistant { | |
| background: white; | |
| padding: 12px 16px; | |
| border-radius: 16px 16px 16px 4px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.06); | |
| border-left: 3px solid var(--accent); | |
| } | |
| .metric-card { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| text-align: center; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.08); | |
| } | |
| .metric-value { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| line-height: 1; | |
| } | |
| .metric-label { | |
| color: #718096; | |
| font-size: 0.85rem; | |
| margin-top: 8px; | |
| } | |
| .trend-up { color: var(--risk-high); } | |
| .trend-down { color: var(--risk-low); } | |
| .trend-stable { color: #718096; } | |
| .detail-section { | |
| background: #F7FAFC; | |
| border-radius: 8px; | |
| padding: 16px; | |
| margin-top: 16px; | |
| } | |
| .detail-label { | |
| font-weight: 600; | |
| color: #1A365D; | |
| margin-bottom: 8px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .detail-label::before { | |
| content: ''; | |
| width: 4px; | |
| height: 20px; | |
| background: var(--accent); | |
| margin-right: 10px; | |
| border-radius: 2px; | |
| } | |
| .plotly-container { | |
| border-radius: 8px; | |
| } | |
| .selectize-input { | |
| border-radius: 8px !important; | |
| } | |
| #send_btn { | |
| background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%); | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| } | |
| #send_btn:hover { | |
| background: linear-gradient(135deg, #E55A25 0%, #FF7B4A 100%); | |
| } | |
| .page-header { | |
| background: linear-gradient(135deg, #1A365D 0%, #2D4A7C 100%); | |
| color: white; | |
| padding: 24px; | |
| border-radius: 12px; | |
| margin-bottom: 24px; | |
| } | |
| .page-header h2 { | |
| margin: 0; | |
| font-weight: 700; | |
| } | |
| .page-header p { | |
| margin: 8px 0 0 0; | |
| opacity: 0.9; | |
| } | |
| ")) | |
| ), | |
| # ----- ダッシュボードタブ ----- | |
| nav_panel( | |
| title = "ダッシュボード", | |
| icon = icon("dashboard"), | |
| div( | |
| class = "page-header", | |
| h2("グローバルリスク概況"), | |
| p("国別の政治・治安リスクをリアルタイムで監視") | |
| ), | |
| layout_columns( | |
| col_widths = c(3, 9), | |
| # 左サイドバー: 国別リスト | |
| card( | |
| card_header("国別リスト"), | |
| div( | |
| style = "max-height: 600px; overflow-y: auto;", | |
| uiOutput("country_list_ui") | |
| ) | |
| ), | |
| # メインコンテンツ | |
| layout_columns( | |
| col_widths = c(12, 12), | |
| # 上部: マップとメトリクス | |
| layout_columns( | |
| col_widths = c(8, 4), | |
| card( | |
| card_header("世界リスクマップ"), | |
| plotlyOutput("risk_map", height = "350px") | |
| ), | |
| layout_columns( | |
| col_widths = 12, | |
| div( | |
| class = "metric-card", | |
| div(class = "metric-value", style = "color: #E53E3E;", textOutput("high_risk_count")), | |
| div(class = "metric-label", "高リスク国 (スコア≥70)") | |
| ), | |
| div( | |
| class = "metric-card", | |
| div(class = "metric-value", style = "color: #DD6B20;", textOutput("medium_risk_count")), | |
| div(class = "metric-label", "中リスク国 (40-69)") | |
| ), | |
| div( | |
| class = "metric-card", | |
| div(class = "metric-value", style = "color: #38A169;", textOutput("low_risk_count")), | |
| div(class = "metric-label", "低リスク国 (<40)") | |
| ) | |
| ) | |
| ), | |
| # 下部: チャートとニュース | |
| layout_columns( | |
| col_widths = c(6, 6), | |
| card( | |
| card_header(textOutput("chart_title")), | |
| plotlyOutput("risk_chart", height = "280px") | |
| ), | |
| card( | |
| card_header("最新ニュースサマリー"), | |
| div( | |
| style = "max-height: 280px; overflow-y: auto;", | |
| uiOutput("news_summary_ui") | |
| ) | |
| ) | |
| ) | |
| ) | |
| ) | |
| ), | |
| # ----- 因果分析タブ ----- | |
| nav_panel( | |
| title = "因果分析", | |
| icon = icon("project-diagram"), | |
| div( | |
| class = "page-header", | |
| h2("因果関係の提示"), | |
| p("「何が起きたか(What)」と「なぜ起きたか(Cause)」を特定") | |
| ), | |
| layout_columns( | |
| col_widths = c(4, 8), | |
| # フィルター | |
| card( | |
| card_header("分析条件"), | |
| selectInput( | |
| "analysis_country", | |
| "対象国を選択", | |
| choices = unique(country_data$country_jp), | |
| selected = "ベネズエラ" | |
| ), | |
| selectInput( | |
| "analysis_category", | |
| "カテゴリ", | |
| choices = c("すべて", unique(news_data$category)), | |
| selected = "すべて" | |
| ), | |
| dateRangeInput( | |
| "date_range", | |
| "期間", | |
| start = "2024-12-01", | |
| end = "2025-01-11", | |
| language = "ja" | |
| ), | |
| hr(), | |
| div( | |
| class = "metric-card", | |
| div( | |
| class = "metric-value", | |
| style = "font-size: 1.8rem;", | |
| textOutput("selected_country_score") | |
| ), | |
| div(class = "metric-label", "現在のリスクスコア"), | |
| div( | |
| style = "margin-top: 10px;", | |
| uiOutput("score_trend_indicator") | |
| ) | |
| ) | |
| ), | |
| # 分析結果 | |
| layout_columns( | |
| col_widths = 12, | |
| card( | |
| card_header("関連ニュース詳細"), | |
| div( | |
| style = "max-height: 600px; overflow-y: auto;", | |
| uiOutput("detailed_news_ui") | |
| ) | |
| ) | |
| ) | |
| ) | |
| ), | |
| # ----- 分析アシスタントタブ ----- | |
| nav_panel( | |
| title = "分析アシスタント", | |
| icon = icon("robot"), | |
| div( | |
| class = "page-header", | |
| h2("分析アシスタント"), | |
| p("リスクに関する質問にAIがお答えします") | |
| ), | |
| layout_columns( | |
| col_widths = c(8, 4), | |
| card( | |
| card_header("チャット"), | |
| div( | |
| id = "chat_container", | |
| class = "chat-container", | |
| uiOutput("chat_messages_ui") | |
| ), | |
| hr(), | |
| layout_columns( | |
| col_widths = c(10, 2), | |
| textInput( | |
| "user_message", | |
| NULL, | |
| placeholder = "質問を入力してください(例:なぜベネズエラのスコアが上がった?)", | |
| width = "100%" | |
| ), | |
| actionButton( | |
| "send_btn", | |
| "送信", | |
| icon = icon("paper-plane"), | |
| class = "btn-primary", | |
| width = "100%" | |
| ) | |
| ) | |
| ), | |
| card( | |
| card_header("よくある質問"), | |
| div( | |
| style = "padding: 8px;", | |
| actionLink("q1", "なぜベネズエラのスコアが上がった?", style = "display: block; padding: 10px; color: #1A365D;"), | |
| hr(style = "margin: 5px 0;"), | |
| actionLink("q2", "イランの最新リスク状況は?", style = "display: block; padding: 10px; color: #1A365D;"), | |
| hr(style = "margin: 5px 0;"), | |
| actionLink("q3", "政変の予兆がある国は?", style = "display: block; padding: 10px; color: #1A365D;"), | |
| hr(style = "margin: 5px 0;"), | |
| actionLink("q4", "中東地域のリスク概況を教えて", style = "display: block; padding: 10px; color: #1A365D;"), | |
| hr(style = "margin: 5px 0;"), | |
| actionLink("q5", "今週スコアが急上昇した国は?", style = "display: block; padding: 10px; color: #1A365D;") | |
| ) | |
| ) | |
| ) | |
| ), | |
| # ----- リスクランキングタブ ----- | |
| nav_panel( | |
| title = "リスクランキング", | |
| icon = icon("list-ol"), | |
| div( | |
| class = "page-header", | |
| h2("国別リスクランキング"), | |
| p("200ヶ国以上の中で「今、どこを気にすべきか」を優先順位付け") | |
| ), | |
| layout_columns( | |
| col_widths = c(6, 6), | |
| card( | |
| card_header("リスクスコア上位国"), | |
| plotlyOutput("ranking_chart", height = "500px") | |
| ), | |
| card( | |
| card_header("全国データ一覧"), | |
| DTOutput("country_table") | |
| ) | |
| ) | |
| ), | |
| nav_spacer(), | |
| nav_item( | |
| tags$span( | |
| style = "color: rgba(255,255,255,0.7); font-size: 0.8rem;", | |
| "プロトタイプ v0.1 " | |
| ) | |
| ) | |
| ) | |
| # ============================================================================= | |
| # Server定義 | |
| # ============================================================================= | |
| server <- function(input, output, session) { | |
| # リアクティブ値 | |
| selected_country <- reactiveVal("Venezuela") | |
| chat_history <- reactiveVal(list( | |
| list( | |
| role = "assistant", | |
| content = "リスクモニタリングアシスタントです。国別のリスクスコアや、スコア変動の要因についてお答えします。何でもお聞きください。" | |
| ) | |
| )) | |
| # ----- 国リストUI ----- | |
| output$country_list_ui <- renderUI({ | |
| sorted_data <- country_data %>% arrange(desc(risk_score)) | |
| lapply(1:nrow(sorted_data), function(i) { | |
| row <- sorted_data[i, ] | |
| score_class <- if (row$risk_score >= 70) "score-high" | |
| else if (row$risk_score >= 40) "score-medium" | |
| else "score-low" | |
| active_class <- if (row$country == selected_country()) "active" else "" | |
| trend_icon <- switch( | |
| row$trend, | |
| "up" = icon("arrow-up", class = "trend-up"), | |
| "down" = icon("arrow-down", class = "trend-down"), | |
| icon("minus", class = "trend-stable") | |
| ) | |
| div( | |
| class = paste("country-list-item", active_class), | |
| onclick = sprintf("Shiny.setInputValue('clicked_country', '%s', {priority: 'event'})", row$country), | |
| div( | |
| style = "display: flex; justify-content: space-between; align-items: center;", | |
| div( | |
| div(style = "font-weight: 600;", row$country_jp), | |
| div(style = "font-size: 0.8rem; color: #718096;", row$country) | |
| ), | |
| div( | |
| style = "display: flex; align-items: center; gap: 8px;", | |
| span(class = score_class, row$risk_score), | |
| trend_icon | |
| ) | |
| ) | |
| ) | |
| }) | |
| }) | |
| # クリックイベント | |
| observeEvent(input$clicked_country, { | |
| selected_country(input$clicked_country) | |
| }) | |
| # ----- リスクマップ ----- | |
| output$risk_map <- renderPlotly({ | |
| # Color based on risk score | |
| colors <- sapply(country_data$risk_score, function(s) { | |
| if (s >= 70) "#E53E3E" | |
| else if (s >= 40) "#DD6B20" | |
| else "#38A169" | |
| }) | |
| plot_geo(country_data, locationmode = 'world') %>% | |
| add_markers( | |
| x = ~lng, | |
| y = ~lat, | |
| size = ~risk_score, | |
| color = I(colors), | |
| text = ~paste0( | |
| "<b>", country_jp, "</b><br>", | |
| "リスクスコア: ", risk_score, "<br>", | |
| "地域: ", region | |
| ), | |
| hoverinfo = "text", | |
| marker = list( | |
| sizemode = 'diameter', | |
| sizeref = 2, | |
| opacity = 0.7, | |
| line = list(width = 1, color = 'white') | |
| ) | |
| ) %>% | |
| layout( | |
| geo = list( | |
| scope = 'world', | |
| projection = list(type = 'natural earth'), | |
| showland = TRUE, | |
| landcolor = toRGB("gray95"), | |
| showocean = TRUE, | |
| oceancolor = toRGB("lightblue"), | |
| showcountries = TRUE, | |
| countrycolor = toRGB("gray80"), | |
| showframe = FALSE | |
| ), | |
| margin = list(l = 0, r = 0, t = 0, b = 0) | |
| ) | |
| }) | |
| # マップの選択国ハイライト(plotlyでは動的ハイライトを簡略化) | |
| # Note: plotlyでは leafletProxy のような動的更新が異なるため、 | |
| # 選択国のハイライトはクリックイベントで対応 | |
| # ----- メトリクス ----- | |
| output$high_risk_count <- renderText({ | |
| sum(country_data$risk_score >= 70) | |
| }) | |
| output$medium_risk_count <- renderText({ | |
| sum(country_data$risk_score >= 40 & country_data$risk_score < 70) | |
| }) | |
| output$low_risk_count <- renderText({ | |
| sum(country_data$risk_score < 40) | |
| }) | |
| # ----- リスクチャート ----- | |
| output$chart_title <- renderText({ | |
| current <- selected_country() | |
| current_jp <- country_data$country_jp[country_data$country == current] | |
| paste0(current_jp, " リスクスコア推移") | |
| }) | |
| output$risk_chart <- renderPlotly({ | |
| current <- selected_country() | |
| base_score <- country_data$risk_score[country_data$country == current] | |
| ts_data <- generate_time_series(current, base_score) | |
| plot_ly(ts_data, x = ~date, y = ~score, type = 'scatter', mode = 'lines', | |
| line = list(color = '#1A365D', width = 2), | |
| fill = 'tozeroy', fillcolor = 'rgba(26, 54, 93, 0.1)') %>% | |
| layout( | |
| xaxis = list(title = "", gridcolor = '#E2E8F0'), | |
| yaxis = list(title = "スコア", range = c(0, 100), gridcolor = '#E2E8F0'), | |
| margin = list(t = 10, b = 40), | |
| hovermode = 'x unified', | |
| plot_bgcolor = 'rgba(0,0,0,0)', | |
| paper_bgcolor = 'rgba(0,0,0,0)' | |
| ) %>% | |
| add_annotations( | |
| x = max(ts_data$date) - 7, | |
| y = max(ts_data$score[ts_data$date > max(ts_data$date) - 14]), | |
| text = if(base_score > 70) "⚠️ Spike" else "", | |
| showarrow = FALSE, | |
| font = list(color = "#E53E3E", size = 14) | |
| ) | |
| }) | |
| # ----- ニュースサマリー ----- | |
| output$news_summary_ui <- renderUI({ | |
| current <- selected_country() | |
| filtered_news <- news_data %>% | |
| filter(country == current) %>% | |
| arrange(desc(date)) %>% | |
| head(5) | |
| if (nrow(filtered_news) == 0) { | |
| return(div(style = "padding: 20px; color: #718096;", "ニュースデータがありません")) | |
| } | |
| lapply(1:nrow(filtered_news), function(i) { | |
| row <- filtered_news[i, ] | |
| div( | |
| class = "news-card", | |
| div( | |
| class = "news-title", | |
| row$title | |
| ), | |
| div( | |
| class = "news-meta", | |
| span(class = paste0("category-badge category-", row$category), row$category), | |
| span(row$date), | |
| span(" | "), | |
| span(row$source) | |
| ) | |
| ) | |
| }) | |
| }) | |
| # ----- 因果分析: 選択国スコア ----- | |
| output$selected_country_score <- renderText({ | |
| country_en <- country_data$country[country_data$country_jp == input$analysis_country] | |
| score <- country_data$risk_score[country_data$country == country_en] | |
| score | |
| }) | |
| output$score_trend_indicator <- renderUI({ | |
| country_en <- country_data$country[country_data$country_jp == input$analysis_country] | |
| trend <- country_data$trend[country_data$country == country_en] | |
| if (trend == "up") { | |
| div(style = "color: #E53E3E;", icon("arrow-up"), " 上昇傾向") | |
| } else if (trend == "down") { | |
| div(style = "color: #38A169;", icon("arrow-down"), " 下降傾向") | |
| } else { | |
| div(style = "color: #718096;", icon("minus"), " 横ばい") | |
| } | |
| }) | |
| # ----- 因果分析: 詳細ニュース ----- | |
| output$detailed_news_ui <- renderUI({ | |
| country_en <- country_data$country[country_data$country_jp == input$analysis_country] | |
| filtered <- news_data %>% | |
| filter(country == country_en) | |
| if (input$analysis_category != "すべて") { | |
| filtered <- filtered %>% filter(category == input$analysis_category) | |
| } | |
| filtered <- filtered %>% | |
| filter(as.Date(date) >= input$date_range[1] & as.Date(date) <= input$date_range[2]) %>% | |
| arrange(desc(date)) | |
| if (nrow(filtered) == 0) { | |
| return(div( | |
| style = "padding: 40px; text-align: center; color: #718096;", | |
| icon("search", style = "font-size: 3rem; margin-bottom: 16px;"), | |
| div("条件に一致するニュースが見つかりませんでした") | |
| )) | |
| } | |
| lapply(1:nrow(filtered), function(i) { | |
| row <- filtered[i, ] | |
| div( | |
| style = "background: white; border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 10px rgba(0,0,0,0.06);", | |
| div( | |
| style = "display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px;", | |
| div( | |
| span(class = paste0("category-badge category-", row$category), row$category), | |
| span(style = "color: #718096; font-size: 0.85rem;", row$date, " | ", row$source) | |
| ), | |
| div( | |
| style = "background: #FED7D7; color: #C53030; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600;", | |
| paste0("影響度: ", row$impact_score, "/10") | |
| ) | |
| ), | |
| div( | |
| style = "font-weight: 600; font-size: 1.1rem; color: #1A365D; margin-bottom: 16px;", | |
| row$title | |
| ), | |
| div( | |
| class = "detail-section", | |
| div(class = "detail-label", "What(何が起きたか)"), | |
| p(style = "margin: 0; color: #4A5568; line-height: 1.7;", row$what) | |
| ), | |
| div( | |
| class = "detail-section", | |
| style = "margin-top: 12px;", | |
| div(class = "detail-label", "Cause(なぜ起きたか)"), | |
| p(style = "margin: 0; color: #4A5568; line-height: 1.7;", row$cause) | |
| ) | |
| ) | |
| }) | |
| }) | |
| # ----- チャットアシスタント ----- | |
| output$chat_messages_ui <- renderUI({ | |
| history <- chat_history() | |
| lapply(history, function(msg) { | |
| div( | |
| class = paste("chat-message", msg$role), | |
| HTML(msg$content) | |
| ) | |
| }) | |
| }) | |
| # チャット送信 | |
| send_message <- function(user_msg) { | |
| if (nchar(trimws(user_msg)) == 0) return() | |
| history <- chat_history() | |
| history <- c(history, list(list(role = "user", content = user_msg))) | |
| # シンプルな応答生成(デモ用) | |
| response <- generate_response(user_msg) | |
| history <- c(history, list(list(role = "assistant", content = response))) | |
| chat_history(history) | |
| updateTextInput(session, "user_message", value = "") | |
| } | |
| observeEvent(input$send_btn, { | |
| send_message(input$user_message) | |
| }) | |
| # よくある質問のクリック | |
| observeEvent(input$q1, { send_message("なぜベネズエラのスコアが上がった?") }) | |
| observeEvent(input$q2, { send_message("イランの最新リスク状況は?") }) | |
| observeEvent(input$q3, { send_message("政変の予兆がある国は?") }) | |
| observeEvent(input$q4, { send_message("中東地域のリスク概況を教えて") }) | |
| observeEvent(input$q5, { send_message("今週スコアが急上昇した国は?") }) | |
| # 応答生成(デモ用のルールベース) | |
| generate_response <- function(query) { | |
| query_lower <- tolower(query) | |
| if (grepl("ベネズエラ", query) && grepl("スコア|上が|なぜ", query)) { | |
| return(paste0( | |
| "<strong>ベネズエラのリスクスコア上昇要因</strong><br><br>", | |
| "ベネズエラのリスクスコアが上昇した主な要因は以下の通りです:<br><br>", | |
| "📰 <strong>政変の予兆</strong><br>", | |
| "2024年大統領選挙の結果を巡る対立が激化しており、野党支持者による大規模な反政府デモがカラカス中心部で発生しています。", | |
| "国際社会からも選挙の正当性に懸念の声が上がっています。<br><br>", | |
| "📰 <strong>経済危機</strong><br>", | |
| "ボリバル通貨が対ドルで過去最安値を更新し、インフレ率が年率200%を超える見通しです。", | |
| "米国の経済制裁による外貨収入の減少と政府の財政政策の失敗が重なっています。<br><br>", | |
| "📎 関連記事:<a href='#'>Political Unrest Reported in Venezuela (2025-01-11)</a>" | |
| )) | |
| } | |
| if (grepl("イラン", query)) { | |
| return(paste0( | |
| "<strong>イランの最新リスク状況</strong><br><br>", | |
| "現在のリスクスコア: <span style='color: #E53E3E; font-weight: bold;'>92</span> (高リスク)<br><br>", | |
| "📰 <strong>主要リスク要因</strong><br>", | |
| "• 西側諸国との核協議が決裂状態<br>", | |
| "• EUによる追加経済制裁の発動<br>", | |
| "• 中東地域での軍事活動の活発化<br><br>", | |
| "特にウラン濃縮活動の継続と、ドローン供与などの軍事支援が国際社会の懸念を高めています。" | |
| )) | |
| } | |
| if (grepl("政変", query) && grepl("予兆|国", query)) { | |
| return(paste0( | |
| "<strong>政変の予兆が検知されている国</strong><br><br>", | |
| "現在、以下の国で「政変の予兆」カテゴリのニュースが検知されています:<br><br>", | |
| "🔴 <strong>ベネズエラ</strong> (スコア: 95)<br>", | |
| "大規模な反政府デモが継続中。選挙結果を巡る対立が先鋭化。<br><br>", | |
| "🔴 <strong>ミャンマー</strong> (スコア: 78)<br>", | |
| "軍事政権による情報統制が強化。民主化運動への弾圧継続。<br><br>", | |
| "🟠 <strong>リビア</strong> (スコア: 72)<br>", | |
| "暫定政府と東部勢力の対話が行き詰まり。" | |
| )) | |
| } | |
| if (grepl("中東", query)) { | |
| return(paste0( | |
| "<strong>中東地域のリスク概況</strong><br><br>", | |
| "中東地域は引き続き高リスク状態が続いています:<br><br>", | |
| "| 国名 | スコア | 傾向 |<br>", | |
| "|------|--------|------|<br>", | |
| "| イラン | 92 | ↑ |<br>", | |
| "| シリア | 88 | → |<br>", | |
| "| イエメン | 85 | ↑ |<br><br>", | |
| "📰 <strong>主要トピック</strong><br>", | |
| "• イラン核協議の行き詰まり<br>", | |
| "• イエメン和平交渉の進展(ただし持続性に疑問)<br>", | |
| "• シリア北部での難民危機の深刻化" | |
| )) | |
| } | |
| if (grepl("急上昇|上昇|今週", query)) { | |
| return(paste0( | |
| "<strong>今週スコアが急上昇した国</strong><br><br>", | |
| "過去7日間でスコアが5ポイント以上上昇した国:<br><br>", | |
| "1️⃣ <strong>ベネズエラ</strong> +12pt (83→95)<br>", | |
| "要因:大規模反政府デモ、通貨危機の深刻化<br><br>", | |
| "2️⃣ <strong>イラン</strong> +7pt (85→92)<br>", | |
| "要因:核協議決裂、追加制裁の発動<br><br>", | |
| "3️⃣ <strong>イエメン</strong> +5pt (80→85)<br>", | |
| "要因:停戦交渉の不透明感" | |
| )) | |
| } | |
| # デフォルト応答 | |
| return(paste0( | |
| "ご質問ありがとうございます。<br><br>", | |
| "現在のシステムでは以下の情報を提供できます:<br>", | |
| "• 国別リスクスコアとその推移<br>", | |
| "• スコア変動の要因となったニュース<br>", | |
| "• カテゴリ別(政変、テロ、経済危機等)の分析<br>", | |
| "• 地域別のリスク概況<br><br>", | |
| "具体的な国名や地域を含めてご質問いただくと、より詳細な情報をお伝えできます。" | |
| )) | |
| } | |
| # ----- ランキングチャート ----- | |
| output$ranking_chart <- renderPlotly({ | |
| top_countries <- country_data %>% | |
| arrange(desc(risk_score)) %>% | |
| head(15) | |
| colors <- sapply(top_countries$risk_score, function(s) { | |
| if (s >= 70) "#E53E3E" | |
| else if (s >= 40) "#DD6B20" | |
| else "#38A169" | |
| }) | |
| plot_ly( | |
| top_countries, | |
| y = ~reorder(country_jp, risk_score), | |
| x = ~risk_score, | |
| type = 'bar', | |
| orientation = 'h', | |
| marker = list(color = colors), | |
| text = ~risk_score, | |
| textposition = 'outside' | |
| ) %>% | |
| layout( | |
| xaxis = list(title = "リスクスコア", range = c(0, 105)), | |
| yaxis = list(title = ""), | |
| margin = list(l = 120), | |
| plot_bgcolor = 'rgba(0,0,0,0)', | |
| paper_bgcolor = 'rgba(0,0,0,0)' | |
| ) | |
| }) | |
| # ----- データテーブル ----- | |
| output$country_table <- renderDT({ | |
| display_data <- country_data %>% | |
| arrange(desc(risk_score)) %>% | |
| mutate( | |
| 順位 = row_number(), | |
| トレンド = case_when( | |
| trend == "up" ~ "↑", | |
| trend == "down" ~ "↓", | |
| TRUE ~ "→" | |
| ) | |
| ) %>% | |
| select(順位, 国名 = country_jp, 英語名 = country, 地域 = region, | |
| リスクスコア = risk_score, トレンド) | |
| datatable( | |
| display_data, | |
| options = list( | |
| pageLength = 10, | |
| language = list(url = '//cdn.datatables.net/plug-ins/1.10.11/i18n/Japanese.json') | |
| ), | |
| rownames = FALSE | |
| ) %>% | |
| formatStyle( | |
| 'リスクスコア', | |
| backgroundColor = styleInterval( | |
| c(40, 70), | |
| c('rgba(56, 161, 105, 0.2)', 'rgba(221, 107, 32, 0.2)', 'rgba(229, 62, 62, 0.2)') | |
| ), | |
| fontWeight = 'bold' | |
| ) | |
| }) | |
| } | |
| # ============================================================================= | |
| # アプリ起動 | |
| # ============================================================================= | |
| shinyApp(ui = ui, server = server) | |