user:
初学者向けにVSCodeを使ってStreamlitアプリを作るガイドを書いてください。そのアプリは、次のような動作をします。
・ユーザにcsvファイルまたはエクセルファイルの入力を促します。
・エクセルファイルの場合は、どのシートを見たいかユーザに確認します。
・読み込まれたデータは、列ごとに時系列を表すと解釈します。時間のデータは含まれているとは限りません。
・Pythonのstatsmodelsで時系列データをトレンド、季節性、残差に分解します。
・ユーザは、列ごとに季節性分解のオプションを別々に設定可能です。
・分解したトレンド、季節性、残差のうち、トレンド成分だけを残すとか、季節成分だけ除いてトレンドと残差を残すなどの処理を、列ごとに指定できます。
assistant:
以下は、初学者向けにVSCodeとStreamlitを使って、時系列分解アプリを作るための丁寧なガイドです。このアプリは、CSVやExcelのファイルを読み込み、各列を時系列データとして解釈し、statsmodels
を使ってトレンド・季節性・残差に分解し、各成分の除去や保持を列ごとに選べるようにします。
🔧 準備編
1. 必要なツールのインストール
以下のものをインストールしてください:
Pythonのインストール
- Python公式サイトからインストール
VSCodeのインストール
- VSCode公式サイトからインストール
Python用拡張機能(VSCode内で)
- 拡張機能 →
Python
を検索してインストール
2. 仮想環境の作成(任意だけど推奨)
python -m venv venv
source venv/bin/activate # Windowsなら venv\Scripts\activate
3. 必要なライブラリをインストール
pip install streamlit pandas openpyxl statsmodels matplotlib
📁 プロジェクト構成
time_series_decompose_app/
│
├── app.py
🧠 app.py
のコード
以下がアプリの本体です:
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
st.title("🕰️ 時系列分解アプリ")
# ファイルアップロード
uploaded_file = st.file_uploader("CSVまたはExcelファイルをアップロードしてください", type=["csv", "xlsx"])
if uploaded_file:
# Excelならシートを選択
if uploaded_file.name.endswith(".xlsx"):
sheet_names = pd.ExcelFile(uploaded_file).sheet_names
selected_sheet = st.selectbox("シートを選択してください", sheet_names)
df = pd.read_excel(uploaded_file, sheet_name=selected_sheet)
else:
df = pd.read_csv(uploaded_file)
st.subheader("📊 読み込んだデータ")
st.dataframe(df)
# 時系列分解
st.subheader("⚙️ 時系列分解と成分選択")
result_dfs = []
for col in df.columns:
if pd.api.types.is_numeric_dtype(df[col]):
st.markdown(f"### 🔍 列: `{col}`")
model = st.selectbox(f"{col}の分解モデルを選択", ["additive", "multiplicative"], key=f"{col}_model")
try:
result = seasonal_decompose(df[col], model=model, period=12)
# プロット
fig, axs = plt.subplots(4, 1, figsize=(8, 6))
result.observed.plot(ax=axs[0], title="Observed")
result.trend.plot(ax=axs[1], title="Trend")
result.seasonal.plot(ax=axs[2], title="Seasonal")
result.resid.plot(ax=axs[3], title="Residual")
plt.tight_layout()
st.pyplot(fig)
# 成分の選択
keep_trend = st.checkbox(f"{col}: トレンドを保持", value=True, key=f"{col}_trend")
keep_seasonal = st.checkbox(f"{col}: 季節性を保持", value=True, key=f"{col}_seasonal")
keep_resid = st.checkbox(f"{col}: 残差を保持", value=True, key=f"{col}_resid")
combined = 0
if keep_trend and result.trend is not None:
combined += result.trend.fillna(0)
if keep_seasonal:
combined += result.seasonal
if keep_resid and result.resid is not None:
combined += result.resid.fillna(0)
result_dfs.append(pd.DataFrame({f"{col}_processed": combined}))
except Exception as e:
st.error(f"{col} の分解に失敗しました: {e}")
# 結果の結合と表示
if result_dfs:
processed_df = pd.concat(result_dfs, axis=1)
st.subheader("📈 加工後のデータ")
st.dataframe(processed_df)
# ダウンロード用
csv = processed_df.to_csv(index=False).encode("utf-8")
st.download_button(
label="📥 加工後のデータをCSVでダウンロード",
data=csv,
file_name="processed_time_series.csv",
mime="text/csv",
)
🚀 アプリの実行方法
VSCodeのターミナルで以下を実行:
streamlit run app.py
ブラウザが開き、アプリが表示されます。
📝 補足
- 分解に使っている
period=12
は「月次データで1年周期」を仮定しています。週次や日次の場合はユーザから設定を受けるように拡張可能です。 - 欠損値処理(
fillna(0)
)をしているので、過度な補完が問題になる場合は要調整。
このガイドをもとに、自分の時系列データで自由に実験できるアプリを作ってみてください!
必要に応じて、ユーザー入力で周期を選べるように拡張することも可能です。やってみますか?
user:
ありがとうございます。Multiplicative seasonality is not appropriate for zero and negative valuesと言われるのですが、この場合、0〜1の範囲にスケーリングしてからMultiplicative seasonalityを計算するという方法を取るのは、良くないでしょうか?
assistant:
良い質問です。「multiplicative seasonality is not appropriate for zero and negative values」という警告は、multiplicative
モデルの特性に由来します:
🔍 なぜmultiplicativeがゼロや負に弱いのか?
multiplicative モデルでは次のような仮定をしています:
observed = trend × seasonal × residual
このため、ゼロや負の値を含むデータでは以下の問題があります:
- 0 があると、どんなに良い seasonal/trend モデルでも掛け算結果は 0。
- 負の値はそもそも「乗算の季節性」という解釈に合わない(振幅が反転する意味になる)。
❓ スケーリングすれば解決する?
たとえば 0〜1 に MinMax スケーリングしたうえで multiplicative
モデルを適用する、という方法はテクニカルには可能です。ただし注意点があります:
⚠️ スケーリングしてmultiplicativeを使う方法の落とし穴
問題点 | 説明 |
---|---|
モデリングの意味が変わる |
multiplicative モデルは「値の変動が水準に比例する」前提です。スケーリングしてしまうと「その構造」を壊してしまうため、multiplicativeの前提自体が破綻します。 |
逆スケーリングが困難 | モデル後に逆スケーリングすると、元の加法的・乗法的構造が失われてしまい、「本当にその分解が妥当だったか」わからなくなります。 |
人工的に負の値を回避しても意味がない | そもそも multiplicative モデルを使いたいのは「データの変動が水準に依存している」時なので、スケーリングしてまで使うのは本末転倒になりがちです。 |
✅ 推奨される対応方法
-
データに負やゼロが含まれるなら
additive
モデルを使う(これが最も自然)。 - もしどうしても
multiplicative
を使いたいなら:- データにゼロが含まれないか確認し、
- 負の値が出ないロジ変換や対数変換(たとえば log(x+1))を適用し、
- それに応じてモデル後の逆変換を検討する。
💡 実践アドバイス
あなたのStreamlitアプリでは、列ごとに自動的にモデル選択を調整することで、ユーザー体験を向上させることができます。例えば:
if df[col].min() <= 0:
st.warning(f"{col} に0以下の値が含まれているため、'additive' モデルを強制します。")
model = "additive"
else:
model = st.selectbox(...)
ご希望であれば、「負の値を含むときは強制的にadditive
を使う」ロジックを追加したサンプルコードを提示できますが、どうしますか?
user:
はい、そうしてください。そして、period=12をユーザが自由に選べるように変更してください。
assistant:
承知しました。以下は、次の2点を反映した改良版コードです:
✅ 追加された機能
-
負またはゼロを含む列は、自動的に
additive
モデルに強制し、ユーザーに通知。 -
period
(季節性周期)をユーザーが自由に設定可能に。
🆕 改良版 app.py
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
st.title("🕰️ 時系列分解アプリ(改良版)")
# ファイルアップロード
uploaded_file = st.file_uploader("CSVまたはExcelファイルをアップロードしてください", type=["csv", "xlsx"])
if uploaded_file:
# Excelならシートを選択
if uploaded_file.name.endswith(".xlsx"):
sheet_names = pd.ExcelFile(uploaded_file).sheet_names
selected_sheet = st.selectbox("シートを選択してください", sheet_names)
df = pd.read_excel(uploaded_file, sheet_name=selected_sheet)
else:
df = pd.read_csv(uploaded_file)
st.subheader("📊 読み込んだデータ")
st.dataframe(df)
st.subheader("⚙️ 分解設定(全体共通)")
period = st.number_input("周期(period)を指定してください(例: 12 = 年間)", min_value=2, value=12, step=1)
result_dfs = []
for col in df.columns:
if pd.api.types.is_numeric_dtype(df[col]):
st.markdown(f"### 🔍 列: `{col}`")
# モデル選択制限
if df[col].min() <= 0:
st.warning(f"{col} に0以下の値が含まれているため、'additive' モデルを強制します。")
model = "additive"
else:
model = st.selectbox(f"{col}の分解モデルを選択", ["additive", "multiplicative"], key=f"{col}_model")
try:
result = seasonal_decompose(df[col], model=model, period=period)
# プロット
fig, axs = plt.subplots(4, 1, figsize=(8, 6))
result.observed.plot(ax=axs[0], title="Observed")
result.trend.plot(ax=axs[1], title="Trend")
result.seasonal.plot(ax=axs[2], title="Seasonal")
result.resid.plot(ax=axs[3], title="Residual")
plt.tight_layout()
st.pyplot(fig)
# 成分の選択
keep_trend = st.checkbox(f"{col}: トレンドを保持", value=True, key=f"{col}_trend")
keep_seasonal = st.checkbox(f"{col}: 季節性を保持", value=True, key=f"{col}_seasonal")
keep_resid = st.checkbox(f"{col}: 残差を保持", value=True, key=f"{col}_resid")
combined = 0
if keep_trend and result.trend is not None:
combined += result.trend.fillna(0)
if keep_seasonal:
combined += result.seasonal
if keep_resid and result.resid is not None:
combined += result.resid.fillna(0)
result_dfs.append(pd.DataFrame({f"{col}_processed": combined}))
except Exception as e:
st.error(f"{col} の分解に失敗しました: {e}")
# 結果の結合と表示
if result_dfs:
processed_df = pd.concat(result_dfs, axis=1)
st.subheader("📈 加工後のデータ")
st.dataframe(processed_df)
# ダウンロード用
csv = processed_df.to_csv(index=False).encode("utf-8")
st.download_button(
label="📥 加工後のデータをCSVでダウンロード",
data=csv,
file_name="processed_time_series.csv",
mime="text/csv",
)
このバージョンなら、ユーザーに不要なエラーを出さずにスムーズに操作できるアプリになります。
さらに改良したい項目があればお知らせください(例:逆変換、ログ変換対応、処理結果のプロットなど)。
user:
ありがとうございます。トレンドを保持、季節性を保持、残差を保持などを選択した時に、その時点で加工後のデータがどう変わったかを可視化するコードを追加してください。
assistant:
喜んで対応いたします。
✅ 今回の改善内容
各列について「トレンド・季節性・残差を保持するかどうか」を選択した際に、それに応じた加工結果(再構成時系列)を即時プロット表示するようにしました。
🔧 変更ポイント
- 各列の分解成分の選択(チェックボックス)後に、再構成された「加工後の時系列」を
matplotlib
でプロット。 - 元の系列と加工後の系列を重ねて比較できるように。
🆕 更新されたコードの該当部分
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
st.title("🕰️ 時系列分解アプリ(加工後の変化も可視化)")
# ファイルアップロード
uploaded_file = st.file_uploader("CSVまたはExcelファイルをアップロードしてください", type=["csv", "xlsx"])
if uploaded_file:
# Excelならシートを選択
if uploaded_file.name.endswith(".xlsx"):
sheet_names = pd.ExcelFile(uploaded_file).sheet_names
selected_sheet = st.selectbox("シートを選択してください", sheet_names)
df = pd.read_excel(uploaded_file, sheet_name=selected_sheet)
else:
df = pd.read_csv(uploaded_file)
st.subheader("📊 読み込んだデータ")
st.dataframe(df)
st.subheader("⚙️ 分解設定(全体共通)")
period = st.number_input("周期(period)を指定してください(例: 12 = 年間)", min_value=2, value=12, step=1)
result_dfs = []
for col in df.columns:
if pd.api.types.is_numeric_dtype(df[col]):
st.markdown(f"### 🔍 列: `{col}`")
# モデル選択制限
if df[col].min() <= 0:
st.warning(f"{col} に0以下の値が含まれているため、'additive' モデルを強制します。")
model = "additive"
else:
model = st.selectbox(f"{col}の分解モデルを選択", ["additive", "multiplicative"], key=f"{col}_model")
try:
result = seasonal_decompose(df[col], model=model, period=period)
# 分解の各成分を可視化
fig1, axs = plt.subplots(4, 1, figsize=(8, 6))
result.observed.plot(ax=axs[0], title="Observed")
result.trend.plot(ax=axs[1], title="Trend")
result.seasonal.plot(ax=axs[2], title="Seasonal")
result.resid.plot(ax=axs[3], title="Residual")
plt.tight_layout()
st.pyplot(fig1)
# 成分の選択
keep_trend = st.checkbox(f"{col}: トレンドを保持", value=True, key=f"{col}_trend")
keep_seasonal = st.checkbox(f"{col}: 季節性を保持", value=True, key=f"{col}_seasonal")
keep_resid = st.checkbox(f"{col}: 残差を保持", value=True, key=f"{col}_resid")
# 成分を合成
combined = 0
if keep_trend and result.trend is not None:
combined += result.trend.fillna(0)
if keep_seasonal:
combined += result.seasonal
if keep_resid and result.resid is not None:
combined += result.resid.fillna(0)
# 加工後の系列のプロット
fig2, ax = plt.subplots(figsize=(8, 3))
df[col].plot(ax=ax, label="Original", linestyle="--")
combined.plot(ax=ax, label="Processed", linewidth=2)
ax.set_title(f"{col}: 加工後の時系列との比較")
ax.legend()
st.pyplot(fig2)
# 保存用データ
result_dfs.append(pd.DataFrame({f"{col}_processed": combined}))
except Exception as e:
st.error(f"{col} の分解に失敗しました: {e}")
# 結果の結合と表示
if result_dfs:
processed_df = pd.concat(result_dfs, axis=1)
st.subheader("📈 加工後のデータ")
st.dataframe(processed_df)
# ダウンロード用
csv = processed_df.to_csv(index=False).encode("utf-8")
st.download_button(
label="📥 加工後のデータをCSVでダウンロード",
data=csv,
file_name="processed_time_series.csv",
mime="text/csv",
)
✅ 実行結果
- 各列に対して「どの成分を保持するか」チェックすると、その構成に応じた加工時系列が即時にプロットされます。
- 元のデータと「加工後の結果」が同じプロット内に描かれ、変化が直感的に分かります。
さらに、チェックボックスの変更に対してリアルタイムでグラフも更新したいといった要望があれば、それも対応できます(例:Streamlitのst.session_state
などの活用)。
ご希望あれば、次のステップに進みましょうか?
user:
ありがとうございます。次のエラーの対処法を教えていただけますか?
There are multiple number_input elements with the same auto-generated ID. When this element is created, it is assigned an internal ID based on the element type and provided parameters. Multiple elements with the same type and parameters will cause this error.
To fix this error, please pass a unique key argument to the number_input element.
assistant:
このエラーは、st.number_input()
に 同じ引数(特に label)で複数回呼び出しが行われている 場合に発生します。Streamlit は内部で要素の識別にラベルを使っているため、同じラベルだと ID がかぶり、エラーになります。
🔧 解決策:key
引数を明示的に指定する
例えば、以下のようにして number_input
にユニークなキー(key=...
)を追加してください。
✅ 修正前(エラーになる例):
period = st.number_input("周期(period)を指定してください", min_value=2, value=12, step=1)
✅ 修正後(ユニークな key
を追加):
period = st.number_input("周期(period)を指定してください", min_value=2, value=12, step=1, key="global_period")
あるいは、列ごとに number_input
を置いている場合は、次のようにすればOKです:
✅ 列ごとに period を設定できるようにしたい場合(動的に key
を付与):
period = st.number_input(f"{col} の周期(period)", min_value=2, value=12, step=1, key=f"{col}_period")
🔁 要点まとめ
-
number_input
に限らず、Streamlit のselectbox
,text_input
,checkbox
などは 同じラベルだけでは区別できない。 - 重複を避けるには、必ず
key
引数に一意な値を与えるようにする。
必要であれば、「列ごとに周期(period)を変えられるようにする」ためのコード全体例も用意できます。ご希望ですか?
user:
「列ごとに周期(period)を変えられるようにする」ためのコード全体例、お願いします。
assistant:
以下に、列ごとに周期(period
)を個別に設定可能な完全な Streamlit アプリのコードを示します。
このアプリでは、ユーザーが CSV または Excel ファイルをアップロードすると、各列ごとに period
、モデル(additive / multiplicative)、保持する成分(トレンド・季節性・残差)を設定できるようになります。
✅ 列ごとに周期を変えられる時系列分解アプリ(完全版)
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
st.title("📈 時系列分解アプリ(列ごとに周期・処理を指定可能)")
# ファイルのアップロード
uploaded_file = st.file_uploader("CSVまたはExcelファイルをアップロードしてください", type=["csv", "xlsx"])
if uploaded_file:
# Excelならシートを選ぶ
if uploaded_file.name.endswith(".xlsx"):
sheet_names = pd.ExcelFile(uploaded_file).sheet_names
selected_sheet = st.selectbox("シートを選択してください", sheet_names)
df = pd.read_excel(uploaded_file, sheet_name=selected_sheet)
else:
df = pd.read_csv(uploaded_file)
st.subheader("📋 読み込んだデータ")
st.dataframe(df)
result_dfs = []
for col in df.columns:
if pd.api.types.is_numeric_dtype(df[col]):
st.markdown(f"---\n### 🔍 列: `{col}`")
# 周期を列ごとに指定(ユニークなkey)
period = st.number_input(f"{col} の周期(例:12 = 年間)", min_value=2, value=12, step=1, key=f"{col}_period")
# モデル選択(0以下の値がある場合は additive 固定)
if df[col].min() <= 0:
st.warning(f"{col} に0以下の値が含まれるため、'additive' モデルを使用します。")
model = "additive"
else:
model = st.selectbox(f"{col} の分解モデル", ["additive", "multiplicative"], key=f"{col}_model")
try:
result = seasonal_decompose(df[col], model=model, period=period)
# 分解結果の可視化
fig1, axs = plt.subplots(4, 1, figsize=(8, 6))
result.observed.plot(ax=axs[0], title="Observed")
result.trend.plot(ax=axs[1], title="Trend")
result.seasonal.plot(ax=axs[2], title="Seasonal")
result.resid.plot(ax=axs[3], title="Residual")
plt.tight_layout()
st.pyplot(fig1)
# 成分を保持するかどうか
keep_trend = st.checkbox(f"{col}: トレンドを保持", value=True, key=f"{col}_keep_trend")
keep_seasonal = st.checkbox(f"{col}: 季節性を保持", value=True, key=f"{col}_keep_seasonal")
keep_resid = st.checkbox(f"{col}: 残差を保持", value=True, key=f"{col}_keep_resid")
# 再構成
combined = 0
if keep_trend and result.trend is not None:
combined += result.trend.fillna(0)
if keep_seasonal:
combined += result.seasonal
if keep_resid and result.resid is not None:
combined += result.resid.fillna(0)
# 加工後のプロット
fig2, ax = plt.subplots(figsize=(8, 3))
df[col].plot(ax=ax, label="Original", linestyle="--")
combined.plot(ax=ax, label="Processed", linewidth=2)
ax.set_title(f"{col}: 加工後の系列との比較")
ax.legend()
st.pyplot(fig2)
result_dfs.append(pd.DataFrame({f"{col}_processed": combined}))
except Exception as e:
st.error(f"{col} の分解に失敗しました: {e}")
# 全列の結果を結合・表示・ダウンロード
if result_dfs:
processed_df = pd.concat(result_dfs, axis=1)
st.subheader("📉 加工後の全列データ")
st.dataframe(processed_df)
csv = processed_df.to_csv(index=False).encode("utf-8")
st.download_button(
label="📥 加工後データをCSVでダウンロード",
data=csv,
file_name="processed_time_series.csv",
mime="text/csv"
)
📌 機能まとめ
機能 | 説明 |
---|---|
列ごとの period 設定 |
各列に異なる周期を指定可能(例:月次と週次を同時処理) |
モデル選択 | 値がすべて正のときに additive / multiplicative 選択可 |
成分の選択 | トレンド / 季節性 / 残差のうち、保持したいものを選択 |
加工後の比較可視化 | 元のデータとの違いを同じグラフで比較表示 |
加工結果のダウンロード | CSV形式で保存可 |
ご希望があれば、次のような機能も追加可能です:
- 加工後のデータを Excel(複数シート)で出力
- グラフを列ごとにタブで切り替え表示
- 時間情報がある場合の自動インデックス認識
追加したい機能があれば教えてください!
user:
ありがとうございます。グラフを列ごとにタブで切り替え表示、お願いします。
assistant:
以下に、各列のグラフ(分解と加工後比較)をタブで切り替えて表示できるようにした完全コードを示します。
タブ表示を使うことで、複数列があっても見やすく整理された UI になります。
✅ 列ごとにタブでグラフ表示する Streamlit アプリ
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
st.set_page_config(layout="wide")
st.title("📈 時系列分解アプリ(列ごとに周期・処理を指定/タブ表示)")
# ファイルのアップロード
uploaded_file = st.file_uploader("CSVまたはExcelファイルをアップロードしてください", type=["csv", "xlsx"])
if uploaded_file:
# Excelならシートを選ぶ
if uploaded_file.name.endswith(".xlsx"):
sheet_names = pd.ExcelFile(uploaded_file).sheet_names
selected_sheet = st.selectbox("シートを選択してください", sheet_names)
df = pd.read_excel(uploaded_file, sheet_name=selected_sheet)
else:
df = pd.read_csv(uploaded_file)
st.subheader("📋 読み込んだデータ")
st.dataframe(df)
result_dfs = []
numeric_columns = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col])]
if not numeric_columns:
st.warning("数値データの列が見つかりませんでした。")
else:
# 列ごとにタブで表示
tabs = st.tabs([f"{col}" for col in numeric_columns])
for col, tab in zip(numeric_columns, tabs):
with tab:
st.markdown(f"### 🔍 列: `{col}`")
# 周期設定
period = st.number_input(f"{col} の周期(例:12 = 年間)", min_value=2, value=12, step=1, key=f"{col}_period")
# モデル選択(0以下の値がある場合は additive 強制)
if df[col].min() <= 0:
st.warning(f"{col} に0以下の値が含まれるため、'additive' モデルを使用します。")
model = "additive"
else:
model = st.selectbox(f"{col} の分解モデル", ["additive", "multiplicative"], key=f"{col}_model")
try:
result = seasonal_decompose(df[col], model=model, period=period)
# 成分保持オプション
keep_trend = st.checkbox(f"{col}: トレンドを保持", value=True, key=f"{col}_keep_trend")
keep_seasonal = st.checkbox(f"{col}: 季節性を保持", value=True, key=f"{col}_keep_seasonal")
keep_resid = st.checkbox(f"{col}: 残差を保持", value=True, key=f"{col}_keep_resid")
# 再構成
combined = 0
if keep_trend and result.trend is not None:
combined += result.trend.fillna(0)
if keep_seasonal:
combined += result.seasonal
if keep_resid and result.resid is not None:
combined += result.resid.fillna(0)
# 分解結果の表示
st.markdown("#### 🔬 分解結果")
fig1, axs = plt.subplots(4, 1, figsize=(8, 6))
result.observed.plot(ax=axs[0], title="Observed")
result.trend.plot(ax=axs[1], title="Trend")
result.seasonal.plot(ax=axs[2], title="Seasonal")
result.resid.plot(ax=axs[3], title="Residual")
plt.tight_layout()
st.pyplot(fig1)
# 加工後の比較
st.markdown("#### 🔁 加工後のデータとの比較")
fig2, ax = plt.subplots(figsize=(8, 3))
df[col].plot(ax=ax, label="Original", linestyle="--")
combined.plot(ax=ax, label="Processed", linewidth=2)
ax.set_title(f"{col}: 加工後の系列との比較")
ax.legend()
st.pyplot(fig2)
# データ格納
result_dfs.append(pd.DataFrame({f"{col}_processed": combined}))
except Exception as e:
st.error(f"{col} の分解に失敗しました: {e}")
# 全結果の結合とダウンロード
if result_dfs:
processed_df = pd.concat(result_dfs, axis=1)
st.subheader("📉 加工後の全列データ")
st.dataframe(processed_df)
csv = processed_df.to_csv(index=False).encode("utf-8")
st.download_button(
label="📥 加工後データをCSVでダウンロード",
data=csv,
file_name="processed_time_series.csv",
mime="text/csv"
)
✅ このバージョンの改善ポイント
改善点 | 内容 |
---|---|
🔄 タブ表示 | 列ごとの処理・グラフをタブで切り替えてスッキリ表示 |
📉 分解&加工比較 | それぞれのグラフを独立して表示して視認性向上 |
🧠 ユニークキー |
key を列ごとに動的生成で ID 重複エラーを回避 |
ご希望であれば、次は「加工結果を Excel の複数シートとして保存」なども可能です。追加したい機能はありますか?
user:
ありがとうございます。ほぼ完成です。加工して、加工した結果を result_dfs として得る部分を関数にして、他のプログラムでも使いまわせるようにしていただけますか?
assistant:
もちろんです!以下に、加工処理を関数として切り出し、他のプログラムからも再利用できるように整理したバージョンを示します。
✅ 加工処理の関数化
まず、列ごとの時系列分解・加工処理をまとめた関数 decompose_and_process_column()
を定義し、それをアプリ内で使うように書き換えます。
🔧 decompose_and_process_column()
の定義
from statsmodels.tsa.seasonal import seasonal_decompose
import pandas as pd
def decompose_and_process_column(series: pd.Series, period: int, model: str,
keep_trend: bool = True,
keep_seasonal: bool = True,
keep_resid: bool = True) -> pd.Series:
"""
単一の時系列データ(pandas.Series)を分解し、指定成分を保持した再構成結果を返す。
Parameters:
- series: 時系列データ(1次元 Series)
- period: 周期(例: 12)
- model: "additive" または "multiplicative"
- keep_trend: トレンド成分を保持するか
- keep_seasonal: 季節成分を保持するか
- keep_resid: 残差成分を保持するか
Returns:
- processed_series: 指定成分で再構成された Series(元と同じ長さ)
"""
result = seasonal_decompose(series, model=model, period=period)
processed = pd.Series(0, index=series.index, dtype="float64")
if keep_trend and result.trend is not None:
processed += result.trend.fillna(0)
if keep_seasonal:
processed += result.seasonal
if keep_resid and result.resid is not None:
processed += result.resid.fillna(0)
return processed
🔁 Streamlitアプリ側での利用例
Streamlit アプリの中では、次のようにこの関数を使います:
from your_module import decompose_and_process_column # 必要に応じて関数を外部ファイルに保存
...
# 関数で処理(try/except内)
processed_series = decompose_and_process_column(
series=df[col],
period=period,
model=model,
keep_trend=keep_trend,
keep_seasonal=keep_seasonal,
keep_resid=keep_resid
)
その後は、これまでと同様にグラフ描画や result_dfs.append(...)
の処理を行います:
result_dfs.append(pd.DataFrame({f"{col}_processed": processed_series}))
✅ 利用例:他スクリプトで再利用
例えば、他のバッチ処理スクリプトなどではこう使えます:
from your_module import decompose_and_process_column
import pandas as pd
df = pd.read_csv("data.csv")
processed = decompose_and_process_column(df["sales"], period=12, model="additive", keep_seasonal=False)
processed.to_csv("processed_sales.csv", index=False)
必要であればこの関数を .py
ファイルに切り出して提供することもできます。保存したいファイル名の希望はありますか?
user:
ありがとうございます。現時点での全コードを書いてください。
assistant:
以下に、現時点での完全なコードを示します。
関数 decompose_and_process_column()
を明示的に切り出しつつ、Streamlitアプリ本体と一体化した形になっています。関数部分を .py
ファイルに分けても使えるよう設計しています。
✅ 完全な Streamlit アプリコード(関数化含む)
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# ---------- 関数定義 ----------
def decompose_and_process_column(series: pd.Series, period: int, model: str,
keep_trend: bool = True,
keep_seasonal: bool = True,
keep_resid: bool = True) -> pd.Series:
"""
単一の時系列データ(pandas.Series)を分解し、指定成分を保持した再構成結果を返す。
Parameters:
- series: 時系列データ(1次元 Series)
- period: 周期(例: 12)
- model: "additive" または "multiplicative"
- keep_trend: トレンド成分を保持するか
- keep_seasonal: 季節成分を保持するか
- keep_resid: 残差成分を保持するか
Returns:
- processed_series: 指定成分で再構成された Series(元と同じ長さ)
"""
result = seasonal_decompose(series, model=model, period=period)
processed = pd.Series(0, index=series.index, dtype="float64")
if keep_trend and result.trend is not None:
processed += result.trend.fillna(0)
if keep_seasonal:
processed += result.seasonal
if keep_resid and result.resid is not None:
processed += result.resid.fillna(0)
return processed, result
# ---------- Streamlit アプリ本体 ----------
st.set_page_config(layout="wide")
st.title("📈 時系列分解アプリ(列ごとに周期・処理を指定/タブ表示)")
uploaded_file = st.file_uploader("CSVまたはExcelファイルをアップロードしてください", type=["csv", "xlsx"])
if uploaded_file:
# Excelならシート選択
if uploaded_file.name.endswith(".xlsx"):
sheet_names = pd.ExcelFile(uploaded_file).sheet_names
selected_sheet = st.selectbox("シートを選択してください", sheet_names)
df = pd.read_excel(uploaded_file, sheet_name=selected_sheet)
else:
df = pd.read_csv(uploaded_file)
st.subheader("📋 読み込んだデータ")
st.dataframe(df)
result_dfs = []
numeric_columns = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col])]
if not numeric_columns:
st.warning("数値データの列が見つかりませんでした。")
else:
tabs = st.tabs([f"{col}" for col in numeric_columns])
for col, tab in zip(numeric_columns, tabs):
with tab:
st.markdown(f"### 🔍 列: `{col}`")
period = st.number_input(f"{col} の周期(例:12 = 年間)", min_value=2, value=12, step=1, key=f"{col}_period")
if df[col].min() <= 0:
st.warning(f"{col} に0以下の値が含まれるため、'additive' モデルを使用します。")
model = "additive"
else:
model = st.selectbox(f"{col} の分解モデル", ["additive", "multiplicative"], key=f"{col}_model")
keep_trend = st.checkbox(f"{col}: トレンドを保持", value=True, key=f"{col}_keep_trend")
keep_seasonal = st.checkbox(f"{col}: 季節性を保持", value=True, key=f"{col}_keep_seasonal")
keep_resid = st.checkbox(f"{col}: 残差を保持", value=True, key=f"{col}_keep_resid")
try:
processed_series, result = decompose_and_process_column(
series=df[col],
period=period,
model=model,
keep_trend=keep_trend,
keep_seasonal=keep_seasonal,
keep_resid=keep_resid
)
# 分解グラフ
st.markdown("#### 🔬 分解結果")
fig1, axs = plt.subplots(4, 1, figsize=(8, 6))
result.observed.plot(ax=axs[0], title="Observed")
result.trend.plot(ax=axs[1], title="Trend")
result.seasonal.plot(ax=axs[2], title="Seasonal")
result.resid.plot(ax=axs[3], title="Residual")
plt.tight_layout()
st.pyplot(fig1)
# 加工比較グラフ
st.markdown("#### 🔁 加工後のデータとの比較")
fig2, ax = plt.subplots(figsize=(8, 3))
df[col].plot(ax=ax, label="Original", linestyle="--")
processed_series.plot(ax=ax, label="Processed", linewidth=2)
ax.set_title(f"{col}: 加工後の系列との比較")
ax.legend()
st.pyplot(fig2)
result_dfs.append(pd.DataFrame({f"{col}_processed": processed_series}))
except Exception as e:
st.error(f"{col} の分解に失敗しました: {e}")
# 結果表示とダウンロード
if result_dfs:
processed_df = pd.concat(result_dfs, axis=1)
st.subheader("📉 加工後の全列データ")
st.dataframe(processed_df)
csv = processed_df.to_csv(index=False).encode("utf-8")
st.download_button(
label="📥 加工後データをCSVでダウンロード",
data=csv,
file_name="processed_time_series.csv",
mime="text/csv"
)
✅ ファイル分割(任意)
もし関数を別ファイル ts_utils.py
に分けるなら:
ts_utils.py
from statsmodels.tsa.seasonal import seasonal_decompose
import pandas as pd
def decompose_and_process_column(series: pd.Series, period: int, model: str,
keep_trend: bool = True,
keep_seasonal: bool = True,
keep_resid: bool = True) -> pd.Series:
result = seasonal_decompose(series, model=model, period=period)
processed = pd.Series(0, index=series.index, dtype="float64")
if keep_trend and result.trend is not None:
processed += result.trend.fillna(0)
if keep_seasonal:
processed += result.seasonal
if keep_resid and result.resid is not None:
processed += result.resid.fillna(0)
return processed, result
そして Streamlit 側で:
from ts_utils import decompose_and_process_column
ご希望があれば、Excel形式で複数シートに保存する機能も追加可能です。必要ですか?
user:
ありがとうございます。今回はこれで大丈夫です。ここまでの会話をブログに記録したいのですが、タイトル案をいくつか提示していただけますか?
assistant:
もちろんです!今回のStreamlitアプリ作成プロセスにふさわしい、初心者にも親しみやすいブログタイトル案をいくつか挙げます。技術内容・読者層を意識して、トーンや切り口を少しずつ変えてあります。
💡 シンプルで親しみやすいタイトル
- 「VSCode × Streamlit で学ぶ!時系列データ分解アプリの作り方」
- 「はじめての時系列分解アプリ開発」
― Streamlitでトレンド・季節性・残差を分けてみよう!
🧠 学び・解説寄りのタイトル
- 「初心者でもできる!Streamlitで学ぶ時系列分解と可視化」
- 「Pythonで時系列を分解してみた:トレンドと季節性を自由に操作するアプリ作り」
- 「statsmodelsで時系列を分解して加工!Streamlitで体験するデータ前処理」
🎯 応用寄り・再利用性を強調したタイトル
- 「時系列分解の再利用可能な関数を作ろう!Streamlitアプリ化まで」
- 「業務データに使える!列ごとに設定を変えて時系列処理するPythonアプリ開発」
🧩 実践感・達成感を押し出すタイトル
- 「加工してわかる、時系列の構造!Streamlitで自作アプリを完成させた話」
- 「CSVをアップして、列ごとに分解・加工・可視化!Streamlitアプリ開発記」
気に入ったタイトルの方向性があれば、サブタイトルや構成も提案できます。どれか気になるものはありますか?
user:
「VSCode × Streamlit で学ぶ!時系列データ分解アプリの作り方」でいきたいと思います。最後に、今回の会話のまとめを詳しく書いてください。
assistant:
承知しました。以下に、ブログ記事「VSCode × Streamlit で学ぶ!時系列データ分解アプリの作り方」用の詳しいまとめを記載します。技術的な内容の振り返りと学びのポイントを網羅しています。
✅ 今回のまとめ
タイトル:VSCode × Streamlit で学ぶ!時系列データ分解アプリの作り方
今回の開発では、Python × Streamlit を使って、時系列データをトレンド・季節性・残差に分解し、列ごとに加工できるアプリを構築しました。特に初心者向けに、VSCodeを開発環境として選定し、ファイルの読み込みから可視化までを段階的に作り込んでいきました。
📌 アプリの主な機能と特徴
機能 | 説明 |
---|---|
ファイル入力 | CSV または Excel ファイルに対応。Excel の場合はシート選択可能。 |
時系列分解 |
statsmodels の seasonal_decompose を使って、各列を「トレンド」「季節性」「残差」に分解。 |
モデル選択 | 各列に対して、「加法モデル(additive)」または「乗法モデル(multiplicative)」を選択可能。 |
周期指定 | 列ごとに周期(period)を自由に設定可能。 |
成分選択 | トレンド・季節性・残差のうち、保持する成分を列ごとに選べる。 |
リアルタイム可視化 | 加工前・加工後の比較を即座に表示。4段階の分解グラフも表示。 |
結果のダウンロード | 加工済みのデータフレームをCSV形式でダウンロード可能。 |
再利用性 | 加工処理部分を関数化し、他のPythonスクリプトからも使えるよう設計。 |
🧠 学んだ技術・ライブラリ
- Streamlit:Webアプリの構築(UIウィジェット、タブ表示、インタラクティブなグラフ)
-
statsmodels:時系列の分解(
seasonal_decompose
) - pandas:ファイル入出力とデータフレーム操作
- matplotlib:時系列グラフの描画
-
Pythonの関数設計:加工処理の再利用可能な関数化(
decompose_and_process_column()
)
🔍 ハマりどころ・解決法
-
「multiplicative モデルは 0 や負の値に使えない」問題
→ 値に0やマイナスが含まれている列は強制的に "additive" モデルを適用するよう制御。 -
Streamlitの「number_inputが重複」エラー
→ 列名を使ってkey=
引数を一意に設定し、同一UI要素の衝突を回避。 -
グラフの表示切り替え
→ 各列をst.tabs()
でタブ化し、UIをコンパクトに。
🔁 汎用性のある関数設計
最終的には、分解と加工処理をまとめた関数:
def decompose_and_process_column(series, period, model, keep_trend, keep_seasonal, keep_resid):
を定義しておくことで、他のアプリや分析スクリプトからも呼び出せるようになりました。これにより「一度作ったコードを使い回せる」という開発効率の良さを体験できました。
✍️ 最後に
Streamlitは、Python初学者でもインタラクティブなアプリが簡単に作れる強力なツールです。今回の時系列分解アプリのように、「データの構造が見える化される」体験は、データ前処理や特徴量設計の感覚を磨くのに非常に役立ちます。
今後は、このアプリをベースに以下のような拡張も考えられます:
- Excel形式での複数シート出力
- トレンドのスムージング手法の切り替え(HPフィルタなど)
- 自動周期推定機能の追加
- Streamlit Cloud等へのデプロイ
ご希望があれば、このまとめをMarkdown形式やブログ記事の草案としても出力できます。必要ですか?
user:
ありがとうございました。今回はこれで大丈夫です。また次回よろしくお願いします。
assistant:
こちらこそ、ありがとうございました!素晴らしいアプリ開発のお手伝いができて嬉しいです。
またいつでもご相談ください。次回も楽しみにしています!どうぞ良い一日を。
user:
あ、ちょっと待ってください。3成分に分解したものを全部足し合わせても、完全には元に戻らないのですが、それ何とかなりませんかね?特に、最初の数期間と最後の数期間です。
assistant:
とても鋭いご質問です。それは**seasonal_decompose
の仕様によるもので、「端のデータが欠ける」問題**です。詳しく説明します。
🔍 なぜ3成分を足しても元に戻らないのか?
statsmodels.tsa.seasonal.seasonal_decompose
は、移動平均などを用いたローカル平滑法に基づいて成分分解を行っています。この際、以下のような事情で 先頭と末尾のデータポイントでトレンドや季節成分が欠ける ことが起こります:
主な原因:
- トレンドは移動平均で計算されるため、端から
period//2
期間分は NaN になる - 季節性の推定にもトレンド除去後のデータが必要なので、結局端の成分も欠損しやすい
- resid = original - trend - seasonal なので、他が欠損すると残差も欠損
✅ 解決策
方法①:extrapolate_trend='freq'
を使う
これは最近の statsmodels
で導入されたオプションで、トレンドを外挿して端を補完します。
decompose_result = seasonal_decompose(series, model=model, period=period, extrapolate_trend='freq')
これにより、端に出てくる NaN
を大幅に減らせます。
方法②:端を削除する前提で加工処理をする
どうしても一部 NaN
が残る場合、それを dropna()
で除くことで、整合性の取れた出力を得られます。
reconstructed = trend + seasonal + resid
reconstructed = reconstructed.dropna()
方法③:より高度な分解手法(例:STL)に切り替える
seasonal_decompose
は単純な手法なので、より精密な分解が必要なら STL
(Seasonal-Trend decomposition using Loess)を使うと良いです。
from statsmodels.tsa.seasonal import STL
stl = STL(series, period=period)
result = stl.fit()
trend = result.trend
seasonal = result.seasonal
resid = result.resid
STLは外挿の扱いが賢く、端まで分解成分がしっかり出ることが多いです。
✅ まとめ
方法 | 説明 | メリット |
---|---|---|
extrapolate_trend='freq' |
端のトレンドを外挿補完 | 一番手軽、既存コードの変更が少ない |
dropna() |
欠損を無視 | 安定した処理だが情報は減る |
STL に変更 |
より高精度で滑らかな分解 | 高度な処理、再現性が高い |
もしご希望あれば、現行コードを STL
に書き換えた例もご提供できます。どの方向で進めたいですか?
user:
ありがとうございます。方法①:extrapolate_trend='freq' を使うか、方法③:より高度な分解手法(例:STL)に切り替えるか、ユーザが選択できるように改変した全コードを書いていただけますか?
assistant:
もちろんです。以下は、ユーザーが「seasonal_decompose(+外挿)」と「STL」を選択できるようにした完全なStreamlitアプリのコードです。
処理の選択は selectbox
で行い、列ごとに処理方法・周期・モデル(加法/乗法)を指定できるようになっています。
✅ 全コード:seasonal_decompose
/ STL
切り替え対応済み
import streamlit as st
import pandas as pd
import numpy as np
from statsmodels.tsa.seasonal import seasonal_decompose, STL
import matplotlib.pyplot as plt
st.set_page_config(layout="wide")
def decompose_and_process_column(series, period, model, method,
keep_trend, keep_seasonal, keep_resid):
if method == "seasonal_decompose":
if model == "multiplicative" and (series <= 0).any():
model = "additive" # 強制的に変更
result = seasonal_decompose(series, period=period, model=model, extrapolate_trend='freq')
trend = result.trend
seasonal = result.seasonal
resid = result.resid
else: # STL
result = STL(series, period=period).fit()
trend = result.trend
seasonal = result.seasonal
resid = result.resid
components = []
if keep_trend:
components.append(trend)
if keep_seasonal:
components.append(seasonal)
if keep_resid:
components.append(resid)
processed = sum(components)
return processed, trend, seasonal, resid
# ファイルアップロード
st.title("時系列分解アプリ")
uploaded_file = st.file_uploader("CSV または Excel ファイルをアップロード", type=["csv", "xlsx"])
if uploaded_file:
if uploaded_file.name.endswith(".xlsx"):
xls = pd.ExcelFile(uploaded_file)
sheet = st.selectbox("シートを選択してください", xls.sheet_names)
df = pd.read_excel(xls, sheet_name=sheet)
else:
df = pd.read_csv(uploaded_file)
st.write("データプレビュー", df.head())
# モデルとメソッドの選択
result_dfs = {}
tabs = st.tabs(df.columns)
for col, tab in zip(df.columns, tabs):
with tab:
st.subheader(f"列: {col}")
method = st.selectbox(f"{col} に適用する分解手法", ["seasonal_decompose", "STL"], key=f"method_{col}")
period = st.number_input(f"{col} の周期(例: 12なら年次、7なら週次)", min_value=2, value=12, key=f"period_{col}")
if method == "seasonal_decompose":
model = st.selectbox(f"{col} のモデル", ["additive", "multiplicative"], key=f"model_{col}")
else:
model = None
keep_trend = st.checkbox("トレンドを保持", value=True, key=f"trend_{col}")
keep_seasonal = st.checkbox("季節性を保持", value=True, key=f"seasonal_{col}")
keep_resid = st.checkbox("残差を保持", value=True, key=f"resid_{col}")
series = df[col].dropna()
processed, trend, seasonal, resid = decompose_and_process_column(
series, period, model, method,
keep_trend, keep_seasonal, keep_resid
)
result_dfs[col] = processed
# 可視化
st.markdown("### 分解結果(元データ・トレンド・季節性・残差)")
fig, axs = plt.subplots(4, 1, figsize=(10, 8), sharex=True)
axs[0].plot(series, label="Original")
axs[0].legend()
axs[1].plot(trend, label="Trend", color="orange")
axs[1].legend()
axs[2].plot(seasonal, label="Seasonal", color="green")
axs[2].legend()
axs[3].plot(resid, label="Residual", color="red")
axs[3].legend()
st.pyplot(fig)
# 加工後のデータ表示
st.markdown("### 加工後データ")
st.line_chart(processed)
# 加工後データをまとめて表示・ダウンロード
combined = pd.DataFrame(result_dfs)
st.markdown("## 全列の加工後データ")
st.dataframe(combined)
csv = combined.to_csv(index=False).encode("utf-8-sig")
st.download_button("加工後データをダウンロード", data=csv, file_name="processed.csv", mime="text/csv")
🔧 ポイント解説
-
method = st.selectbox(...)
で列ごとに「seasonal_decompose」または「STL」を選択可能 -
seasonal_decompose
の場合はextrapolate_trend='freq'
を必ず指定 -
model
はseasonal_decompose
のときのみ選択表示 - 出力されたトレンド・季節性・残差・加工後の合成を可視化
- 最後に加工後データを統合してCSVでダウンロード可能
必要に応じて、このコードを複数ページ構成のStreamlitアプリに発展させることもできます。ご希望あればその案もご提案できます!
user:
ありがとうございます。次のエラーが出たので対処してください。
ValueError: The truth value of a Index is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
Traceback:
File "/Users/ikemen/streamlit-app/venv/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/exec_code.py", line 121, in exec_func_with_error_handling
result = func()
File "/Users/ikemen/streamlit-app/venv/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 639, in code_to_exec
_mpa_v1(self._main_script_path)
File "/Users/ikemen/streamlit-app/venv/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 159, in _mpa_v1
page.run()
File "/Users/ikemen/streamlit-app/venv/lib/python3.10/site-packages/streamlit/navigation/page.py", line 297, in run
exec(code, module.__dict__)
File "/Users/ikemen/streamlit-app/pages/decompose.py", line 51, in <module>
tabs = st.tabs(df.columns)
File "/Users/ikemen/streamlit-app/venv/lib/python3.10/site-packages/streamlit/runtime/metrics_util.py", line 410, in wrapped_func
result = non_optional_func(*args, **kwargs)
File "/Users/ikemen/streamlit-app/venv/lib/python3.10/site-packages/streamlit/elements/layouts.py", line 467, in tabs
if not tabs:
File "/Users/ikemen/streamlit-app/venv/lib/python3.10/site-packages/pandas/core/indexes/base.py", line 3190, in __nonzero__
raise ValueError(
assistant:
このエラーは、次の行で df.columns
(= pandas の Index
オブジェクト)を直接 st.tabs()
に渡していることが原因です。
tabs = st.tabs(df.columns) # ❌ NG:Indexオブジェクトの真偽値は曖昧
Streamlit の st.tabs()
には Python の文字列リスト を渡す必要があります。
したがって、以下のように df.columns.tolist()
に変換して使ってください。
✅ 修正箇所
tabs = st.tabs(df.columns.tolist())
これでエラーは解消するはずです。
🔁 解説
-
df.columns
はpandas.Index
であって、真偽値(if df.columns:
など)として評価できません。 -
.tolist()
を使って普通のリストに変換すれば、Streamlit でも正しく処理されます。
他にも不具合が出たら遠慮なく教えてくださいね。
user:
ありがとうございます!では、また今度!
assistant:
どういたしまして!こちらこそ、楽しくご一緒できて嬉しかったです。
またいつでもご相談くださいね。良い開発ライフを!😊👋