Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- Dockerfile +21 -0
- README.md +29 -5
- app_fast.py +375 -0
- processed_analysis_data.parquet +3 -0
- requirements.txt +5 -0
- tab6_analysis_master.parquet +3 -0
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 依存ライブラリのインストール
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# アプリ本体とデータ(parquet)をコピー
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
# Streamlit設定(HF Spaces向け:ポート8501・外部公開・使用統計オフ)
|
| 13 |
+
ENV STREAMLIT_SERVER_PORT=8501 \
|
| 14 |
+
STREAMLIT_SERVER_ADDRESS=0.0.0.0 \
|
| 15 |
+
STREAMLIT_SERVER_HEADLESS=true \
|
| 16 |
+
STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \
|
| 17 |
+
HOME=/app
|
| 18 |
+
|
| 19 |
+
EXPOSE 8501
|
| 20 |
+
|
| 21 |
+
CMD ["streamlit", "run", "app_fast.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,34 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: パチンコ統合分析システム
|
| 3 |
+
emoji: 🎰
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 8501
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# パチンコ・スロット統合分析システム(高速版)
|
| 12 |
+
|
| 13 |
+
事前生成した parquet を読み込んで分析する Streamlit アプリです。
|
| 14 |
+
Hugging Face Spaces では Docker SDK で動かします。
|
| 15 |
+
|
| 16 |
+
## 必要なファイル
|
| 17 |
+
- `Dockerfile` … 起動設定(Streamlit を 8501 で起動)
|
| 18 |
+
- `app_fast.py` … アプリ本体
|
| 19 |
+
- `requirements.txt` … 依存ライブラリ
|
| 20 |
+
- 以下の parquet(`build_data.py` を手元で実行して生成)
|
| 21 |
+
- `processed_analysis_data.parquet`(メイン分析データ)
|
| 22 |
+
- `tab6_analysis_master.parquet`(新台導入分析用)
|
| 23 |
+
|
| 24 |
+
## パスワードの設定
|
| 25 |
+
このアプリは簡易パスワードで保護されています。
|
| 26 |
+
Space の **Settings → Variables and secrets** で、以下のシークレットを追加してください。
|
| 27 |
+
|
| 28 |
+
- Name: `app_password`
|
| 29 |
+
- Value: 任意の共有パスワード
|
| 30 |
+
|
| 31 |
+
## データ更新の手順
|
| 32 |
+
1. 手元の PC で最新の CSV を `data/` に置き、`python build_data.py` を実行
|
| 33 |
+
2. 生成された parquet を、この Space にアップロード(差し替え)
|
| 34 |
+
3. Space が自動で再起動し、最新データが反映されます
|
app_fast.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
|
| 7 |
+
st.set_page_config(page_title="パチンコ統合分析システム", layout="wide")
|
| 8 |
+
|
| 9 |
+
# --- 簡易パスワード認証 ---
|
| 10 |
+
def _get_app_password():
|
| 11 |
+
"""パスワードの取得元。HF(Docker)では環境変数、ローカルでは secrets.toml。"""
|
| 12 |
+
pw = os.environ.get("app_password")
|
| 13 |
+
if pw:
|
| 14 |
+
return pw
|
| 15 |
+
try:
|
| 16 |
+
return st.secrets["app_password"]
|
| 17 |
+
except Exception:
|
| 18 |
+
return None
|
| 19 |
+
|
| 20 |
+
def check_password():
|
| 21 |
+
"""共有パスワードによる簡易認証。正しいパスワードが入るまで本体を表示しない。"""
|
| 22 |
+
def password_entered():
|
| 23 |
+
if st.session_state.get("password", "") == _get_app_password():
|
| 24 |
+
st.session_state["auth_ok"] = True
|
| 25 |
+
del st.session_state["password"] # パスワードを保持しない
|
| 26 |
+
else:
|
| 27 |
+
st.session_state["auth_ok"] = False
|
| 28 |
+
|
| 29 |
+
if st.session_state.get("auth_ok", False):
|
| 30 |
+
return True
|
| 31 |
+
|
| 32 |
+
st.text_input("パスワードを入力してください", type="password",
|
| 33 |
+
on_change=password_entered, key="password")
|
| 34 |
+
if st.session_state.get("auth_ok") is False:
|
| 35 |
+
st.error("パスワードが違います。")
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
if not check_password():
|
| 39 |
+
st.stop()
|
| 40 |
+
# --- 認証ここまで ---
|
| 41 |
+
|
| 42 |
+
# --- 強制右詰めCSS ---
|
| 43 |
+
st.markdown(
|
| 44 |
+
"""
|
| 45 |
+
<style>
|
| 46 |
+
[data-testid="stTableCell"] { text-align: right !important; justify-content: flex-end !important; }
|
| 47 |
+
[data-testid="stHeaderCell"] { text-align: right !important; }
|
| 48 |
+
[data-testid="stTableCell"]:nth-child(1), [data-testid="stTableCell"]:nth-child(2), [data-testid="stTableCell"]:nth-child(3),
|
| 49 |
+
[data-testid="stHeaderCell"]:nth-child(1), [data-testid="stHeaderCell"]:nth-child(2), [data-testid="stHeaderCell"]:nth-child(3) {
|
| 50 |
+
text-align: left !important;
|
| 51 |
+
justify-content: flex-start !important;
|
| 52 |
+
}
|
| 53 |
+
</style>
|
| 54 |
+
""",
|
| 55 |
+
unsafe_allow_html=True
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
@st.cache_data
|
| 59 |
+
def load_final_data():
|
| 60 |
+
if not os.path.exists("processed_analysis_data.parquet"):
|
| 61 |
+
return None
|
| 62 |
+
df = pd.read_parquet("processed_analysis_data.parquet")
|
| 63 |
+
|
| 64 |
+
# --- 列名の標準化 ---
|
| 65 |
+
rename_map = {
|
| 66 |
+
"パチンコタイプ名": "タイプ名_P",
|
| 67 |
+
"パチンコタイプID": "ソートID_P",
|
| 68 |
+
"スロットタイプ名": "タイプ名_S",
|
| 69 |
+
"スロットタイプID": "ソートID_S"
|
| 70 |
+
}
|
| 71 |
+
df = df.rename(columns=rename_map)
|
| 72 |
+
|
| 73 |
+
# 型の統一
|
| 74 |
+
if "Pcode" in df.columns:
|
| 75 |
+
df["Pcode"] = df["Pcode"].astype(str)
|
| 76 |
+
|
| 77 |
+
for c in ["ソートID_P", "ソートID_S"]:
|
| 78 |
+
if c in df.columns:
|
| 79 |
+
df[c] = pd.to_numeric(df[c], errors='coerce').fillna(999)
|
| 80 |
+
|
| 81 |
+
for col in ["店舗ID", "グループID", "都道府県ID"]:
|
| 82 |
+
if col in df.columns:
|
| 83 |
+
df[col] = df[col].astype(str)
|
| 84 |
+
|
| 85 |
+
if "販売年月日" in df.columns:
|
| 86 |
+
df["販売年月日"] = pd.to_datetime(df["販売年月日"].astype(str), errors='coerce').dt.strftime('%Y/%m/%d')
|
| 87 |
+
df["販売年月日"] = df["販売年月日"].fillna("-")
|
| 88 |
+
|
| 89 |
+
# 資産価値計算用の数値化
|
| 90 |
+
if "中古単価" in df.columns:
|
| 91 |
+
df["中古単価_num"] = pd.to_numeric(df["中古単価"], errors='coerce').fillna(0)
|
| 92 |
+
if "設置台数" in df.columns:
|
| 93 |
+
df["設置台数"] = pd.to_numeric(df["設置台数"], errors='coerce').fillna(0)
|
| 94 |
+
|
| 95 |
+
return df
|
| 96 |
+
|
| 97 |
+
@st.cache_data
|
| 98 |
+
def load_shindai_data():
|
| 99 |
+
target = "tab6_analysis_master.parquet"
|
| 100 |
+
if os.path.exists(target):
|
| 101 |
+
try:
|
| 102 |
+
df = pd.read_parquet(target)
|
| 103 |
+
df['導入月'] = pd.to_datetime(df['導入月'])
|
| 104 |
+
return df
|
| 105 |
+
except Exception as e:
|
| 106 |
+
st.error(f"データ読み込みエラー: {e}")
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
# --- メイン処理 ---
|
| 110 |
+
df = load_final_data()
|
| 111 |
+
|
| 112 |
+
if df is None:
|
| 113 |
+
st.error("分析用データが見つかりません。build_data.pyを実行してください。")
|
| 114 |
+
else:
|
| 115 |
+
st.title("🎰 パチンコ・スロット統合分析 (高速版)")
|
| 116 |
+
|
| 117 |
+
# --- サイドバー ---
|
| 118 |
+
st.sidebar.title("分析軸選択")
|
| 119 |
+
axis_options = [c for c in ["店舗ID", "グループID", "グループ名", "都道府県ID"] if c in df.columns]
|
| 120 |
+
axis = st.sidebar.selectbox("集計軸", axis_options)
|
| 121 |
+
|
| 122 |
+
if axis == "店舗ID" and "店舗名" in df.columns:
|
| 123 |
+
id_name_map = df.drop_duplicates("店舗ID").set_index("店舗ID")["店舗名"].to_dict()
|
| 124 |
+
options = ["すべて"] + [f"{tid}: {id_name_map.get(tid, '')}" for tid in sorted(df["店舗ID"].unique())]
|
| 125 |
+
selected_option = st.sidebar.selectbox("表示対象の店舗を選択", options)
|
| 126 |
+
selected_value = selected_option.split(":")[0] if selected_option != "すべて" else "すべて"
|
| 127 |
+
else:
|
| 128 |
+
selected_value = st.sidebar.selectbox(f"表示対象の{axis}を選択", ["すべて"] + sorted(df[axis].unique().tolist()))
|
| 129 |
+
|
| 130 |
+
menu = st.sidebar.radio("メニュー", [
|
| 131 |
+
"貸玉別集計",
|
| 132 |
+
"競合比較分析",
|
| 133 |
+
"資産価値分析",
|
| 134 |
+
"設置機種詳細",
|
| 135 |
+
"タイプ別集計",
|
| 136 |
+
"新台導入分析"
|
| 137 |
+
])
|
| 138 |
+
|
| 139 |
+
# --- 1. 貸玉別集計 ---
|
| 140 |
+
if menu == "貸玉別集計":
|
| 141 |
+
st.subheader(f"📊 {axis}別 設置台数")
|
| 142 |
+
temp_df = df if selected_value == "すべて" else df[df[axis] == selected_value]
|
| 143 |
+
if not temp_df.empty:
|
| 144 |
+
st.dataframe(temp_df.groupby([axis, '貸玉区分']).size().unstack(fill_value=0), use_container_width=True)
|
| 145 |
+
|
| 146 |
+
# --- 2. 競合比較分析---
|
| 147 |
+
elif menu == "競合比較分析":
|
| 148 |
+
st.subheader("⚔️ 商圏内 詳細構成分析 & レポート")
|
| 149 |
+
|
| 150 |
+
if selected_value == "すべて" or axis != "店舗ID":
|
| 151 |
+
st.info("サイドバーで特定の「店舗ID」を選択すると、詳細な商圏比較を表示します。")
|
| 152 |
+
else:
|
| 153 |
+
target_shop_id = selected_value
|
| 154 |
+
|
| 155 |
+
# --- A. 基礎データの作成 (店舗単位サマリー) ---
|
| 156 |
+
# 1. 全国集計
|
| 157 |
+
national_shop_sum = df.groupby("店舗ID").agg({"設置台数":"sum"})
|
| 158 |
+
for cat in ["通常P", "低貸P", "通常S", "低貸S"]:
|
| 159 |
+
national_shop_sum[cat] = df[df["貸玉区分"]==cat].groupby("店舗ID")["設置台数"].sum()
|
| 160 |
+
national_shop_sum = national_shop_sum.fillna(0)
|
| 161 |
+
|
| 162 |
+
# 2. 全国・都道府県平均比率の算出
|
| 163 |
+
def calc_avg_ratios(source_df):
|
| 164 |
+
t = source_df["設置台数"].sum()
|
| 165 |
+
if t == 0: return [0,0,0,0]
|
| 166 |
+
return [
|
| 167 |
+
(source_df["通常P"].sum() / t * 100),
|
| 168 |
+
(source_df["低貸P"].sum() / t * 100),
|
| 169 |
+
(source_df["通常S"].sum() / t * 100),
|
| 170 |
+
(source_df["低貸S"].sum() / t * 100)
|
| 171 |
+
]
|
| 172 |
+
|
| 173 |
+
avg_national = calc_avg_ratios(national_shop_sum)
|
| 174 |
+
|
| 175 |
+
p_id = df[df["店舗ID"] == target_shop_id]["都道府県ID"].iloc[0]
|
| 176 |
+
pref_shops = national_shop_sum.loc[national_shop_sum.index.isin(df[df["都道府県ID"]==p_id]["店舗ID"].unique())]
|
| 177 |
+
avg_pref = calc_avg_ratios(pref_shops)
|
| 178 |
+
|
| 179 |
+
# 3. エリア(商圏)サマリーの作成
|
| 180 |
+
# ※ここでは例として「同じ都道府県内」をエリアとしていますが、距離データがある場合はここでフィルタリング
|
| 181 |
+
area_summary = pref_shops.copy()
|
| 182 |
+
area_summary["店舗名"] = df.drop_duplicates("店舗ID").set_index("店舗ID")["店舗名"]
|
| 183 |
+
avg_area = calc_avg_ratios(area_summary)
|
| 184 |
+
|
| 185 |
+
# --- B. グラフ用データの作成 (平均データの差し込み) ---
|
| 186 |
+
target_shop_row = area_summary.loc[target_shop_id]
|
| 187 |
+
target_ratios = [
|
| 188 |
+
(target_shop_row["通常P"] / target_shop_row["設置台数"] * 100),
|
| 189 |
+
(target_shop_row["低貸P"] / target_shop_row["設置台数"] * 100),
|
| 190 |
+
(target_shop_row["通常S"] / target_shop_row["設置台数"] * 100),
|
| 191 |
+
(target_shop_row["低貸S"] / target_shop_row["設置台数"] * 100)
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
# 比較用データフレーム
|
| 195 |
+
compare_ratios_df = pd.DataFrame([
|
| 196 |
+
{"区分": "選択店", "P通常": target_ratios[0], "P低貸": target_ratios[1], "S通常": target_ratios[2], "S低貸": target_ratios[3]},
|
| 197 |
+
{"区分": "エリア平均", "P通常": avg_area[0], "P低貸": avg_area[1], "S通常": avg_area[2], "S低貸": avg_area[3]},
|
| 198 |
+
{"区分": "都道府県平均", "P通常": avg_pref[0], "P低貸": avg_pref[1], "S通常": avg_pref[2], "S低貸": avg_pref[3]},
|
| 199 |
+
{"区分": "全国平均", "P通常": avg_national[0], "P低貸": avg_national[1], "S通常": avg_national[2], "S低貸": avg_national[3]},
|
| 200 |
+
])
|
| 201 |
+
|
| 202 |
+
# --- C. 表示処理 ---
|
| 203 |
+
st.write("#### ⚖️ 構成比率の比較(全体100%)")
|
| 204 |
+
st.bar_chart(compare_ratios_df.set_index("区分"), use_container_width=True)
|
| 205 |
+
|
| 206 |
+
# --- 表示セクション ---
|
| 207 |
+
col1, col2 = st.columns([1, 1])
|
| 208 |
+
|
| 209 |
+
with col1:
|
| 210 |
+
st.markdown(f"#### 🏟️ {target_shop_name} の部門構成")
|
| 211 |
+
shop_row = area_summary[area_summary["店舗ID"]==target_shop_id].iloc[0]
|
| 212 |
+
|
| 213 |
+
fig = go.Figure()
|
| 214 |
+
categories = ['通常P', '低貸P', '通常S', '低貸S']
|
| 215 |
+
fig.add_trace(go.Bar(
|
| 216 |
+
y=categories,
|
| 217 |
+
x=[shop_row['通常P'], shop_row['低貸P'], shop_row['通常S'], shop_row['低貸S']],
|
| 218 |
+
orientation='h',
|
| 219 |
+
marker_color=['#d62728', '#ff9896', '#1f77b4', '#aec7e8']
|
| 220 |
+
))
|
| 221 |
+
fig.update_layout(height=300, margin=dict(l=20, r=20, t=20, b=20))
|
| 222 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 223 |
+
|
| 224 |
+
with col2:
|
| 225 |
+
st.markdown("#### 📝 戦略アセスメント")
|
| 226 |
+
p_rank = area_summary["通常P"].rank(ascending=False).iloc[0]
|
| 227 |
+
total_shops = len(area_summary)
|
| 228 |
+
st.success(f"**商圏内ポジション:** 通常P設置台数 第{int(p_rank)}位 / {total_shops}店舗中")
|
| 229 |
+
|
| 230 |
+
# 自動コメント生成
|
| 231 |
+
if shop_row["通常S"] > shop_row["通常P"] * 0.8:
|
| 232 |
+
comment = "スロット比率が高く、若年層や高稼働層をターゲットとした構成です。"
|
| 233 |
+
else:
|
| 234 |
+
comment = "パチンコ主体の安定型構成です。地域密着型の営業スタイルが推測されます。"
|
| 235 |
+
st.write(comment)
|
| 236 |
+
|
| 237 |
+
st.markdown("---")
|
| 238 |
+
st.markdown("#### 🛰️ 周辺店舗との比較リスト")
|
| 239 |
+
st.dataframe(area_summary.sort_values("設置台数", ascending=False), use_container_width=True, hide_index=True)
|
| 240 |
+
|
| 241 |
+
# --- 3. 資産価値分析 ---
|
| 242 |
+
elif menu == "資産価値分析":
|
| 243 |
+
st.subheader("🏢 店舗別・1台あたり資産価値比較")
|
| 244 |
+
# (既存の資産価値ロジックを維持)
|
| 245 |
+
df_val = df[["店舗ID", "店舗名", "都道府県ID", "グループID", "貸玉区分", "中古単価_num", "設置台数"]].copy()
|
| 246 |
+
df_val["資産総額"] = df_val["中古単価_num"] * df_val["設置台数"]
|
| 247 |
+
cat_map = {"全体": ["通常P", "低貸P", "通常S", "低貸S"], "通常パチンコ": ["通常P"], "通常スロット": ["通常S"]}
|
| 248 |
+
|
| 249 |
+
def calc_unit_fast(target_df, label):
|
| 250 |
+
res = {"区分": label}
|
| 251 |
+
for name, cats in cat_map.items():
|
| 252 |
+
tmp = target_df[target_df["貸玉区分"].isin(cats)]
|
| 253 |
+
t_a, t_q = tmp["資産総額"].sum(), tmp["設置台数"].sum()
|
| 254 |
+
res[name] = t_a / t_q if t_q > 0 else 0
|
| 255 |
+
return res
|
| 256 |
+
|
| 257 |
+
if selected_value != "すべて":
|
| 258 |
+
selected_df = df_val[df_val[axis] == selected_value]
|
| 259 |
+
comparison_rows = [calc_unit_fast(df_val, "🌏 全国平均")]
|
| 260 |
+
if "都道府県ID" in selected_df.columns and not selected_df.empty:
|
| 261 |
+
p_id = selected_df["都道府県ID"].iloc[0]
|
| 262 |
+
comparison_rows.append(calc_unit_fast(df_val[df_val["都道府県ID"] == p_id], "🗾 都道府県平均"))
|
| 263 |
+
comparison_rows.append(calc_unit_fast(selected_df, f"★選択中: {selected_value}"))
|
| 264 |
+
display_compare_df = pd.DataFrame(comparison_rows)
|
| 265 |
+
else:
|
| 266 |
+
agg_list = []
|
| 267 |
+
for name, cats in cat_map.items():
|
| 268 |
+
f_df = df_val[df_val["貸玉区分"].isin(cats)]
|
| 269 |
+
if f_df.empty: continue
|
| 270 |
+
agg = f_df.groupby(axis).agg({"資産総額":"sum", "設置台数":"sum"})
|
| 271 |
+
agg[name] = agg["資産総額"] / agg["設置台数"]
|
| 272 |
+
agg_list.append(agg[[name]])
|
| 273 |
+
display_compare_df = pd.concat(agg_list, axis=1).fillna(0).reset_index().rename(columns={axis: "区分"})
|
| 274 |
+
|
| 275 |
+
st.dataframe(display_compare_df.style.format(precision=0, thousands=","), use_container_width=True, hide_index=True)
|
| 276 |
+
|
| 277 |
+
# --- 4. 設置機種詳細 ---
|
| 278 |
+
elif menu == "設置機種詳細":
|
| 279 |
+
st.subheader("🎰 設置機種・市場比較レポート")
|
| 280 |
+
# (既存の設置機種詳細ロジックを維持)
|
| 281 |
+
neighbor_ids = []
|
| 282 |
+
if selected_value != "すべて" and axis == "店舗ID":
|
| 283 |
+
p_id = df[df["店舗ID"] == selected_value]["都道府県ID"].iloc[0]
|
| 284 |
+
neighbor_ids = df[df["都道府県ID"] == p_id]["店舗ID"].unique().tolist()
|
| 285 |
+
if selected_value in neighbor_ids:
|
| 286 |
+
neighbor_ids.remove(selected_value)
|
| 287 |
+
neighbor_ids = [selected_value] + neighbor_ids
|
| 288 |
+
|
| 289 |
+
categories = {"🔴 通常P": "通常P", "🟢 低貸P": "低貸P", "🔵 通常S": "通常S", "🟡 低貸S": "低貸S"}
|
| 290 |
+
sub_tabs = st.tabs(list(categories.keys()))
|
| 291 |
+
|
| 292 |
+
for i, (label, internal_name) in enumerate(categories.items()):
|
| 293 |
+
with sub_tabs[i]:
|
| 294 |
+
tab_df = df[df["貸玉区分"] == internal_name].copy()
|
| 295 |
+
if selected_value == "すべて":
|
| 296 |
+
display_df = tab_df
|
| 297 |
+
elif neighbor_ids:
|
| 298 |
+
display_df = tab_df[tab_df["店舗ID"].isin(neighbor_ids)]
|
| 299 |
+
else:
|
| 300 |
+
display_df = tab_df[tab_df[axis] == selected_value]
|
| 301 |
+
|
| 302 |
+
if display_df.empty:
|
| 303 |
+
st.info(f"{label} のデータがありません。")
|
| 304 |
+
continue
|
| 305 |
+
|
| 306 |
+
agg_cols = {"機種名": "first", "メーカー名": "first", "販売年月日": "first", "中古単価": "max", "全国台数": "max", "都道府県台数": "max"}
|
| 307 |
+
extra = ["確率区分", "スマパチ", "デカヘソ", "機種タイプ区分", "スマスロ", "30パイ", "BT機"]
|
| 308 |
+
for c in extra:
|
| 309 |
+
if c in display_df.columns: agg_cols[c] = "first"
|
| 310 |
+
|
| 311 |
+
summary = display_df.groupby("Pcode").agg(agg_cols)
|
| 312 |
+
col_axis = "店舗名" if "店舗名" in display_df.columns else axis
|
| 313 |
+
pivot = display_df.pivot_table(index="Pcode", columns=col_axis, values="設置台数", aggfunc="sum", fill_value=0)
|
| 314 |
+
|
| 315 |
+
if neighbor_ids:
|
| 316 |
+
id_to_name = display_df.drop_duplicates("店舗ID").set_index("店舗ID")["店舗名"].to_dict()
|
| 317 |
+
target_order = [id_to_name[tid] for tid in neighbor_ids if tid in id_to_name and id_to_name[tid] in pivot.columns]
|
| 318 |
+
pivot = pivot.reindex(columns=target_order, fill_value=0)
|
| 319 |
+
|
| 320 |
+
final_display = pd.concat([summary, pivot], axis=1)
|
| 321 |
+
shop_names = pivot.columns.tolist()
|
| 322 |
+
final_display["商圏合計"] = final_display[shop_names].sum(axis=1)
|
| 323 |
+
|
| 324 |
+
sum_row = pd.Series(index=final_display.columns, dtype=object).fillna("")
|
| 325 |
+
sum_row["機種名"] = "■店舗合計台数"
|
| 326 |
+
for s_col in shop_names: sum_row[s_col] = final_display[s_col].sum()
|
| 327 |
+
sum_row["商圏合計"] = final_display["商圏合計"].sum()
|
| 328 |
+
|
| 329 |
+
output_df = pd.concat([pd.DataFrame([sum_row]), final_display.sort_values("商圏合計", ascending=False)], ignore_index=True)
|
| 330 |
+
st.dataframe(output_df, use_container_width=True, height=650, hide_index=True)
|
| 331 |
+
|
| 332 |
+
# --- 5. タイプ別集計 ---
|
| 333 |
+
elif menu == "タイプ別集計":
|
| 334 |
+
st.subheader("📊 タイプ別設置構成レポート")
|
| 335 |
+
categories = {"🔴 通常P": "通常P", "🟢 低貸P": "低貸P", "🔵 通常S": "通常S", "🟡 低貸S": "低貸S"}
|
| 336 |
+
sub_tabs = st.tabs(list(categories.keys()))
|
| 337 |
+
for i, (label, internal_name) in enumerate(categories.items()):
|
| 338 |
+
with sub_tabs[i]:
|
| 339 |
+
tab_df = df[df["貸玉区分"] == internal_name].copy()
|
| 340 |
+
if tab_df.empty: continue
|
| 341 |
+
suffix = "P" if "P" in internal_name else "S"
|
| 342 |
+
t_col = f"タイプ名_{suffix}"
|
| 343 |
+
actual_t_col = t_col if t_col in tab_df.columns else ("確率区分" if suffix == "P" else "機種タイプ区分")
|
| 344 |
+
|
| 345 |
+
display_label = "店舗名" if "店舗名" in tab_df.columns else axis
|
| 346 |
+
pivot_count = tab_df.pivot_table(index=display_label, columns=actual_t_col, values="設置台数", aggfunc="sum", fill_value=0)
|
| 347 |
+
pivot_count["■合計"] = pivot_count.sum(axis=1)
|
| 348 |
+
st.dataframe(pivot_count.sort_values("■合計", ascending=False), use_container_width=True)
|
| 349 |
+
|
| 350 |
+
# --- 6. 新台導入分析 ---
|
| 351 |
+
elif menu == "新台導入分析":
|
| 352 |
+
st.subheader("📅 新台導入推移")
|
| 353 |
+
shindai_df = load_shindai_data()
|
| 354 |
+
if shindai_df is not None:
|
| 355 |
+
plot_df = shindai_df if selected_value == "すべて" else shindai_df[shindai_df[axis] == selected_value]
|
| 356 |
+
if plot_df.empty:
|
| 357 |
+
st.warning("データがありません。")
|
| 358 |
+
else:
|
| 359 |
+
plot_df["月"] = plot_df["導入月"].dt.strftime('%Y/%m')
|
| 360 |
+
selected_kind = st.selectbox("種別", ["店舗全体", "パチンコ", "スロット"])
|
| 361 |
+
view_df = plot_df[plot_df["種別"] == selected_kind]
|
| 362 |
+
|
| 363 |
+
if not view_df.empty:
|
| 364 |
+
pivot_table = view_df.pivot_table(index=["店舗ID", "店舗名"], columns="月", values="導入台数", aggfunc="sum", fill_value=0)
|
| 365 |
+
sorted_cols = sorted(pivot_table.columns, reverse=True)
|
| 366 |
+
pivot_table = pivot_table.reindex(columns=sorted_cols)
|
| 367 |
+
display_pivot = pivot_table.reset_index()
|
| 368 |
+
st.dataframe(
|
| 369 |
+
display_pivot,
|
| 370 |
+
use_container_width=True,
|
| 371 |
+
hide_index=True,
|
| 372 |
+
column_config={col: st.column_config.NumberColumn(format="%d") for col in sorted_cols}
|
| 373 |
+
)
|
| 374 |
+
else:
|
| 375 |
+
st.error("新台分析用ファイルが見つかりません。")
|
processed_analysis_data.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:853fde6e2dacf6a02e802340a9ab7400f252897764b91817e4c385089ff719ad
|
| 3 |
+
size 22704892
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.58.0
|
| 2 |
+
pandas>=2.0
|
| 3 |
+
numpy>=1.24
|
| 4 |
+
plotly>=5.18
|
| 5 |
+
pyarrow>=14.0
|
tab6_analysis_master.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0bc33ebb94c6e02f69cbb6c28b2221a3a88a7ecd05f6d1a4135d0a8d22a19e2d
|
| 3 |
+
size 10552242
|