1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

時系列データを自在に作れるStreamlitアプリ

Posted at

user:

先日、時系列っぽいデータを人工的に作るプログラムを書きました。

import numpy as np
import matplotlib.pyplot as plt

def sum_of_sigmoids(A, x):
    """
    A: M×3の行列 (numpy.ndarray)
       - A[:, 0] -> B (スケール)
       - A[:, 1] -> C (傾き)
       - A[:, 2] -> -D/C (変曲点のx座標)
    x: 計算する点 (float または numpy.ndarray)

    返り値:
        M個のシグモイド関数の和
    """
    A = np.asarray(A)  # 念のため ndarray に変換
    if A.shape[1] != 3:
        raise ValueError("A は M×3 の行列でなければなりません。")
    
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]  # 変曲点のx座標 (-D/C)
    D = -C * x_inflection  # Dを計算
    
    # M個のシグモイド関数を計算し、それらの合計を返す
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)

def sum_of_sin(A, x):
    """
    A: M×3の行列 (numpy.ndarray)
       - A[:, 0] -> 振幅 (Amplitude)
       - A[:, 1] -> 周期 (Period)
       - A[:, 2] -> 位相 (Phase)
    x: 計算する点 (float または numpy.ndarray)

    返り値:
        M個のsin関数の和
    """
    A = np.asarray(A)  # 念のため ndarray に変換
    if A.shape[1] != 3:
        raise ValueError("A は M×3 の行列でなければなりません。")
    
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    
    # M個の sin 関数を計算し、それらの合計を返す
    return np.sum(amplitudes[:, None] * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]), axis=0)

def moving_average(y, window_size):
    """
    移動平均を計算する関数
    y: 平滑化するデータ (numpy.ndarray)
    window_size: 移動平均のウィンドウサイズ (int)
    """
    return np.convolve(y, np.ones(window_size)/window_size, mode='valid')

# 使用例
A = np.array([[1, 0.1, 100],  
              [0.5, -1, 200], 
              [2, 0.2, 300]])

B = np.array([[0.1, 7, 0],  
              [0.2, 31, np.pi/4],  
              [0.3, 365, np.pi/2]]) 

x = np.linspace(0, 365, 365)  
y = sum_of_sigmoids(A, x) + sum_of_sin(B, x)

今回、VSCode と Streamlit を使って、次の要求を満たすアプリを作ってください。

・ユーザは「日次」「月次」「年次」「何年分」などのパラメータを入力する。
・ユーザがパラメータを入力すると、時系列データを合成し図示を行う。
・気に入ったデータは、エクセルデータとして保存できる。
・異なる時系列データは、1つのエクセル上の異なるシートに保存する。

assistant:

いいですね!Streamlit と VSCode を使って、ユーザーが入力したパラメータに応じて時系列データを合成・表示し、さらに Excel ファイルに保存するアプリを作る構成は以下のようになります。


✅ アプリの概要機能

  1. パラメータ入力

    • 時系列の粒度(「日次」「月次」「年次」)
    • 期間(何年分)
  2. 時系列データの生成

    • あなたの sum_of_sigmoidssum_of_sin を使う
  3. グラフ表示

    • matplotlib または st.line_chart で表示
  4. Excel への保存

    • ユーザーが「保存」ボタンを押すと、複数のデータセットを Excel の異なるシートに保存

💻 Streamlit アプリのコード例(app.py

import streamlit as st
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO

# --- 関数定義(sum_of_sigmoids, sum_of_sin, moving_average) ---
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)

def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(amplitudes[:, None] * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]), axis=0)

def moving_average(y, window_size):
    return np.convolve(y, np.ones(window_size)/window_size, mode='valid')

# --- Streamlit UI ---
st.title("人工時系列データ合成アプリ")

freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
years = st.number_input("何年分のデータを生成しますか?", min_value=1, max_value=100, value=3)
save_name = st.text_input("保存するデータの名前(Excelシート名)", value="dataset1")

if freq == "日次":
    num_points = years * 365
elif freq == "月次":
    num_points = years * 12
elif freq == "年次":
    num_points = years
else:
    num_points = 365

x = np.linspace(0, num_points, num_points)

# 例としてパラメータ固定(ユーザー入力対応にも変更可)
A_sigmoid = np.array([[1, 0.1, 100], [0.5, -1, 200], [2, 0.2, 300]])
A_sin = np.array([[0.1, 7, 0], [0.2, 31, np.pi/4], [0.3, 365, np.pi/2]])

# データ合成
y = sum_of_sigmoids(A_sigmoid, x) + sum_of_sin(A_sin, x)
df = pd.DataFrame({"x": x, "y": y})

# プロット
st.line_chart(df.set_index("x"))

# Excel書き出し用バッファ
if 'datasets' not in st.session_state:
    st.session_state.datasets = {}

if st.button("このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("全データを Excel に書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(label="Excel ファイルをダウンロード",
                       data=output.getvalue(),
                       file_name="synthetic_timeseries.xlsx",
                       mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

✅ 実行方法(VSCode)

  1. 上記コードを app.py として保存
  2. ターミナルで次を実行:
streamlit run app.py

✅ 補足(今後の拡張案)

  • パラメータ A_sigmoid, A_sin を UI で設定できるようにする
  • 周期的変化だけのバージョン、トレンドだけのバージョンも生成
  • 作成データを CSV としても保存できるようにする

こういう方向性で進めて OK そう?カスタムしたいポイントがあれば、そこから拡張していきましょう!

user:

ありがとうございます。「例としてパラメータ固定(ユーザー入力対応にも変更可)」とありますが、ユーザがパラメータを好きに変更できるようにコードを書き換えてください。

assistant:

了解しました!それでは、ユーザーがシグモイド関数とサイン関数のパラメータ(3行×3列の数値)を自由に入力できるように、Streamlit UI を拡張してみましょう。


✅ 変更点

  • ユーザーが NumPy 配列の形でシグモイドとサインのパラメータをテキスト入力できるようにする。
  • 入力形式の例を表示。
  • 入力が不正な場合はエラーを表示。

🔧 修正版コード(UIでパラメータ入力)

以下を app.py に上書きしてください:

import streamlit as st
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO

# --- データ生成関数 ---
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)

def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(amplitudes[:, None] * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]), axis=0)

def moving_average(y, window_size):
    return np.convolve(y, np.ones(window_size)/window_size, mode='valid')

# --- Streamlit UI ---
st.title("人工時系列データ合成アプリ")

# 時系列粒度と期間
freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
years = st.number_input("何年分のデータを生成しますか?", min_value=1, max_value=100, value=3)
save_name = st.text_input("保存するデータの名前(Excelシート名)", value="dataset1")

# サンプルパラメータを例示
example_sigmoid = "1, 0.1, 100\n0.5, -1, 200\n2, 0.2, 300"
example_sin = "0.1, 7, 0\n0.2, 31, 0.785\n0.3, 365, 1.57"

st.markdown("#### シグモイド関数のパラメータ(B, C, 変曲点x)を入力:")
sigmoid_text = st.text_area("シグモイドパラメータ (1行に1組、カンマ区切り)", value=example_sigmoid)

st.markdown("#### サイン関数のパラメータ(振幅, 周期, 位相)を入力:")
sin_text = st.text_area("サインパラメータ (1行に1組、カンマ区切り)", value=example_sin)

def parse_parameter(text):
    try:
        lines = text.strip().split('\n')
        return np.array([[float(x) for x in line.split(',')] for line in lines])
    except Exception as e:
        st.error(f"パラメータの読み込み中にエラーが発生しました: {e}")
        return None

A_sigmoid = parse_parameter(sigmoid_text)
A_sin = parse_parameter(sin_text)

# 時系列生成
if freq == "日次":
    num_points = years * 365
elif freq == "月次":
    num_points = years * 12
elif freq == "年次":
    num_points = years
else:
    num_points = 365

x = np.linspace(0, num_points, num_points)

# データ生成と表示
if A_sigmoid is not None and A_sin is not None:
    y = sum_of_sigmoids(A_sigmoid, x) + sum_of_sin(A_sin, x)
    df = pd.DataFrame({"x": x, "y": y})
    st.line_chart(df.set_index("x"))

    # データ保存
    if 'datasets' not in st.session_state:
        st.session_state.datasets = {}

    if st.button("このデータを保存する"):
        st.session_state.datasets[save_name] = df
        st.success(f"{save_name} を保存しました。")

    if st.button("全データを Excel に書き出す"):
        output = BytesIO()
        with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
            for sheet_name, data in st.session_state.datasets.items():
                data.to_excel(writer, index=False, sheet_name=sheet_name)
        st.download_button(label="Excel ファイルをダウンロード",
                        data=output.getvalue(),
                        file_name="synthetic_timeseries.xlsx",
                        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

✅ 入力例(そのままコピペでOK)

シグモイド関数の入力例

1, 0.1, 100
0.5, -1, 200
2, 0.2, 300

サイン関数の入力例

0.1, 7, 0
0.2, 31, 0.785
0.3, 365, 1.57

この形なら、どんな時系列でも柔軟に作れて、ユーザーの自由度がぐっと上がります!

必要なら「パラメータをGUIで入れる」(例えばスライダーで1つずつ設定)方法にもできますよ。どうしましょうか?

user:

ありがとうございます。「パラメータをGUIで入れる」方法に変更してください。

assistant:

了解しました!では、ユーザーが 各シグモイド関数・サイン関数のパラメータを GUI(スライダーや数値入力)で設定できるようにコードを変更します。


✅ 変更内容

  • シグモイド・サイン関数の「数」を選択(例: 3つ)
  • 各関数に対して、スケール/傾き/変曲点(シグモイド)や 振幅/周期/位相(サイン)を GUI で設定
  • GUI で設定した値を np.array に変換して利用

💻 GUI版 Streamlit アプリ(app.py

import streamlit as st
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO

# --- データ生成関数 ---
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)

def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(amplitudes[:, None] * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]), axis=0)

def moving_average(y, window_size):
    return np.convolve(y, np.ones(window_size)/window_size, mode='valid')

# --- Streamlit UI ---
st.title("人工時系列データ合成アプリ(GUI入力対応)")

freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
years = st.number_input("何年分のデータを生成しますか?", min_value=1, max_value=100, value=3)
save_name = st.text_input("保存するデータの名前(Excelシート名)", value="dataset1")

if freq == "日次":
    num_points = years * 365
elif freq == "月次":
    num_points = years * 12
elif freq == "年次":
    num_points = years
else:
    num_points = 365

x = np.linspace(0, num_points, num_points)

# --- シグモイド関数の入力 ---
st.subheader("シグモイド関数のパラメータ入力")
num_sigmoid = st.number_input("シグモイド関数の数", min_value=1, max_value=10, value=3)
sigmoid_params = []

for i in range(num_sigmoid):
    st.markdown(f"##### シグモイド関数 {i+1}")
    B = st.slider(f"B (スケール) #{i+1}", min_value=-10.0, max_value=10.0, value=1.0, step=0.1)
    C = st.slider(f"C (傾き) #{i+1}", min_value=-5.0, max_value=5.0, value=0.1, step=0.1)
    x0 = st.slider(f"変曲点x #{i+1}", min_value=0.0, max_value=float(num_points), value=100.0, step=1.0)
    sigmoid_params.append([B, C, x0])

A_sigmoid = np.array(sigmoid_params)

# --- サイン関数の入力 ---
st.subheader("サイン関数のパラメータ入力")
num_sin = st.number_input("サイン関数の数", min_value=1, max_value=10, value=3)
sin_params = []

for i in range(num_sin):
    st.markdown(f"##### サイン関数 {i+1}")
    A = st.slider(f"振幅 #{i+1}", min_value=0.0, max_value=10.0, value=0.5, step=0.1)
    P = st.slider(f"周期 #{i+1}", min_value=1.0, max_value=float(num_points), value=30.0, step=1.0)
    phase = st.slider(f"位相 #{i+1} (ラジアン)", min_value=0.0, max_value=2*np.pi, value=0.0, step=0.1)
    sin_params.append([A, P, phase])

A_sin = np.array(sin_params)

# --- データ合成と可視化 ---
y = sum_of_sigmoids(A_sigmoid, x) + sum_of_sin(A_sin, x)
df = pd.DataFrame({"x": x, "y": y})
st.line_chart(df.set_index("x"))

# --- Excel保存処理 ---
if 'datasets' not in st.session_state:
    st.session_state.datasets = {}

if st.button("このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("全データを Excel に書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(label="Excel ファイルをダウンロード",
                       data=output.getvalue(),
                       file_name="synthetic_timeseries.xlsx",
                       mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

🎛️ GUI のメリット

  • 入力ミスを防げる
  • グラフを見ながら調整しやすい
  • 初心者にも優しい

もし、

  • 「パラメータセットを複数保存して切り替えたい」
  • 「週次や四半期の粒度を追加したい」
  • 「関数ごとにノイズを加えたい」

などあれば、すぐ追加できます!どうしましょうか?

user:

ありがとうございます。横軸を「何年何月何日」にしほしいです。また、ユーザが設定した大きさのランダムウォークノイズを加えることができるようにしてください。

assistant:

了解です!以下の2点を実装します:


✅ 追加要件対応

  1. 横軸を「日付形式(年月日)」に変更
     → 日付の開始日を st.date_input() で指定し、pd.date_range() を使って x を日付にします。

  2. ランダムウォークノイズの追加
     → ユーザーが「ノイズの大きさ(標準偏差)」を GUI で設定できるようにして、累積和でランダムウォークを生成。


🔄 修正後のStreamlitコード(追記・修正部分のみ)

以下、差分を中心に説明しますが、まるごと置き換えてもOKです。

# --- Streamlit UI ---

st.title("人工時系列データ合成アプリ(GUI+日付+ノイズ)")

# ① 粒度・期間の指定
freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
years = st.number_input("何年分のデータを生成しますか?", min_value=1, max_value=100, value=3)
start_date = st.date_input("開始日を選んでください", value=pd.to_datetime("2020-01-01"))

save_name = st.text_input("保存するデータの名前(Excelシート名)", value="dataset1")

# ② x軸の生成(date_rangeを使う)
if freq == "日次":
    dates = pd.date_range(start=start_date, periods=years*365, freq='D')
elif freq == "月次":
    dates = pd.date_range(start=start_date, periods=years*12, freq='M')
elif freq == "年次":
    dates = pd.date_range(start=start_date, periods=years, freq='Y')
else:
    dates = pd.date_range(start=start_date, periods=365, freq='D')

x_numeric = np.linspace(0, len(dates), len(dates))  # モデル計算用
x_datetime = dates                                  # 表示・保存用

# ③ ランダムウォークノイズの強さ設定
st.subheader("ノイズの設定(ランダムウォーク)")
noise_std = st.slider("ノイズの大きさ(標準偏差)", min_value=0.0, max_value=5.0, value=0.0, step=0.1)

# --- データ合成と可視化 ---
y = sum_of_sigmoids(A_sigmoid, x_numeric) + sum_of_sin(A_sin, x_numeric)

# ④ ノイズ追加(ランダムウォーク)
if noise_std > 0:
    noise = np.random.normal(loc=0, scale=noise_std, size=len(y))
    noise = np.cumsum(noise)  # ランダムウォークに変換
    y += noise

df = pd.DataFrame({"Date": x_datetime, "Value": y})
st.line_chart(df.set_index("Date"))

💡 補足

  • 横軸は st.line_chart(df.set_index("Date")) にすることで自動的に「日付表示」になります。
  • ノイズは np.random.normal → np.cumsum によって「ランダムウォーク(累積ノイズ)」になっています。
  • シード値を固定して再現性を確保したい場合は np.random.seed(42) を入れてください。

✅ 追加機能アイデア(希望あれば対応します)

  • グラフにノイズ成分のみも別色で表示
  • 日付の粒度(週次・四半期)追加
  • 保存時に「ノイズあり・なし」両方出力
  • サンプルデータの読み込みや保存・再利用

やってみて、表示が思った通りにならない・もうちょい改良したいところがあれば、気軽に教えてください!

user:

ありがとうございます。ユーザがパラメータを操作している間にインタラクティブにグラフが変わるのは良いことなのですが、パラメータを操作する画面が大きすぎて、グラフのインタラクティブな変化を確認しづらいので、パラメータを操作する画面をコンパクトにしていただけませんか?修正後の全コードを書いてください。

assistant:

了解しました!
今回は以下のように改善して、パラメータ入力エリアをコンパクトに表示します。


✅ 主な改善ポイント

  1. シグモイド・サイン関数のパラメータ入力を折りたたみ(st.expander
  2. 入力フィールドを横並びで配置(st.columns
  3. 画面上部にグラフが来るよう順序を調整

✅ 完全修正版コード(app.py

import streamlit as st
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO

# データ生成関数
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)

def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(amplitudes[:, None] * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]), axis=0)

# ランダムウォークノイズ
def generate_random_walk(length, std_dev):
    noise = np.random.normal(loc=0, scale=std_dev, size=length)
    return np.cumsum(noise)

# Streamlit UI
st.set_page_config(layout="wide")
st.title("🧪 人工時系列データ合成アプリ")

# 左サイドバー:全体パラメータ
with st.sidebar:
    st.header("🔧 全体設定")
    freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
    years = st.number_input("何年分のデータを生成しますか?", min_value=1, max_value=100, value=3)
    start_date = st.date_input("開始日", value=pd.to_datetime("2020-01-01"))
    save_name = st.text_input("保存名(Excelシート名)", value="dataset1")
    noise_std = st.slider("ノイズの大きさ(標準偏差)", 0.0, 5.0, 0.0, step=0.1)

# 日付・x軸の生成
if freq == "日次":
    dates = pd.date_range(start=start_date, periods=years * 365, freq='D')
elif freq == "月次":
    dates = pd.date_range(start=start_date, periods=years * 12, freq='M')
else:
    dates = pd.date_range(start=start_date, periods=years, freq='Y')

x_numeric = np.linspace(0, len(dates), len(dates))
x_datetime = dates

# メインレイアウト
col_graph, col_params = st.columns([2.5, 1.5])

with col_params:
    st.subheader("📈 関数パラメータ")

    # シグモイド
    with st.expander("シグモイド関数"):
        num_sigmoid = st.number_input("", 1, 5, 3, key="sigmoid_count")
        sigmoid_params = []
        for i in range(num_sigmoid):
            st.markdown(f"**Sigmoid #{i+1}**")
            cols = st.columns(3)
            B = cols[0].number_input(f"B{i+1}", -10.0, 10.0, 1.0, 0.1, key=f"B{i}")
            C = cols[1].number_input(f"C{i+1}", -5.0, 5.0, 0.1, 0.1, key=f"C{i}")
            x0 = cols[2].number_input(f"x0{i+1}", 0.0, float(len(x_numeric)), 100.0, 1.0, key=f"x0{i}")
            sigmoid_params.append([B, C, x0])
        A_sigmoid = np.array(sigmoid_params)

    # サイン
    with st.expander("サイン関数"):
        num_sin = st.number_input("", 1, 5, 3, key="sin_count")
        sin_params = []
        for i in range(num_sin):
            st.markdown(f"**Sin #{i+1}**")
            cols = st.columns(3)
            A = cols[0].number_input(f"A{i+1}", 0.0, 10.0, 0.5, 0.1, key=f"A{i}")
            P = cols[1].number_input(f"P{i+1}", 1.0, float(len(x_numeric)), 30.0, 1.0, key=f"P{i}")
            phase = cols[2].number_input(f"位相{i+1}", 0.0, 2 * np.pi, 0.0, 0.1, key=f"phase{i}")
            sin_params.append([A, P, phase])
        A_sin = np.array(sin_params)

# グラフ描画
with col_graph:
    y = sum_of_sigmoids(A_sigmoid, x_numeric) + sum_of_sin(A_sin, x_numeric)

    if noise_std > 0:
        noise = generate_random_walk(len(y), noise_std)
        y += noise

    df = pd.DataFrame({"Date": x_datetime, "Value": y})
    st.line_chart(df.set_index("Date"))

# Excel保存セクション
st.subheader("💾 データ保存")
if 'datasets' not in st.session_state:
    st.session_state.datasets = {}

if st.button("✅ このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("📥 Excel に全データを書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(label="📂 ダウンロード",
                       data=output.getvalue(),
                       file_name="synthetic_timeseries.xlsx",
                       mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

✅ 使い方の流れ(サマリ)

  1. 画面左のサイドバーでデータ粒度や年数、開始日などを設定
  2. **右側の「パラメータ」欄(折りたたみ式)**で関数の形を調整
  3. 左側のグラフをリアルタイムに確認
  4. 保存名を入力して「✅ このデータを保存」
  5. 「📥 Excel に書き出す」で全データを一括保存!

改善点や追加してみたい機能があれば、いつでもどうぞ!

user:

ありがとうございます。いただいたコードを少しだけ改変して、次のようにしました。

from io import BytesIO

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import streamlit as st


# データ生成関数
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)


def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(
        amplitudes[:, None]
        * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]),
        axis=0,
    )


# ランダムウォークノイズ
def generate_random_walk(length, std_dev):
    noise = np.random.normal(loc=0, scale=std_dev, size=length)
    return np.cumsum(noise)


# Streamlit UI
st.set_page_config(layout="wide")
st.title("🧪 人工時系列データ合成アプリ")

# 左サイドバー:全体パラメータ
with st.sidebar:
    st.header("🔧 全体設定")
    freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
    years = st.number_input(
        "何年分のデータを生成しますか?", min_value=1, max_value=100, value=3
    )
    start_date = st.date_input("開始日", value=pd.to_datetime("2020-01-01"))
    save_name = st.text_input("保存名(Excelシート名)", value="dataset1")
    noise_std = st.slider("ノイズの大きさ(標準偏差)", 0.0, 5.0, 0.0, step=0.1)

# 日付・x軸の生成
if freq == "日次":
    dates = pd.date_range(start=start_date, periods=years * 365, freq="D")
elif freq == "月次":
    dates = pd.date_range(start=start_date, periods=years * 12, freq="M")
else:
    dates = pd.date_range(start=start_date, periods=years, freq="Y")

x_numeric = np.linspace(0, len(dates), len(dates))
x_datetime = dates

# メインレイアウト
col_graph, col_params = st.columns([2.5, 1.5])

with col_params:
    st.subheader("📈 関数パラメータ")

    # シグモイド
    with st.expander("シグモイド関数"):
        num_sigmoid = st.number_input("", 1, 5, 3, key="sigmoid_count")
        sigmoid_params = []
        for i in range(num_sigmoid):
            st.markdown(f"**Sigmoid #{i+1}**")
            cols = st.columns(3)
            B = cols[0].number_input(
                f"スケール{i+1}", -10.0, 10.0, float(i + 1), 0.1, key=f"B{i}"
            )
            C = cols[1].number_input(
                f"傾き{i+1}", -5.0, 5.0, float(0.3 + (i - 0.5)), 0.1, key=f"C{i}"
            )
            x0 = cols[2].number_input(
                f"変曲点{i+1}",
                0.0,
                float(len(x_numeric)),
                float((i + 1) * 200),
                1.0,
                key=f"x0{i}",
            )
            sigmoid_params.append([B, C, x0])
        A_sigmoid = np.array(sigmoid_params)

    # サイン
    with st.expander("サイン関数"):
        num_sin = st.number_input("", 1, 5, 3, key="sin_count")
        sin_params = []
        freqs = [7.0, 31.0, 365.1, 29.1]
        for i in range(num_sin):
            st.markdown(f"**Sin #{i+1}**")
            cols = st.columns(3)
            A = cols[0].number_input(
                f"振幅{i+1}", 0.0, 10.0, float(i + 1), 0.1, key=f"A{i}"
            )
            P = cols[1].number_input(
                f"周期{i+1}",
                1.0,
                float(len(x_numeric)),
                freqs[i % len(freqs)],
                1.0,
                key=f"P{i}",
            )
            phase = cols[2].number_input(
                f"位相{i+1}", 0.0, 2 * np.pi, float(i + 1), 0.1, key=f"phase{i}"
            )
            sin_params.append([A, P, phase])
        A_sin = np.array(sin_params)

# グラフ描画
with col_graph:
    y = sum_of_sigmoids(A_sigmoid, x_numeric) + sum_of_sin(A_sin, x_numeric)

    if noise_std > 0:
        noise = generate_random_walk(len(y), noise_std)
        y += noise

    df = pd.DataFrame({"Date": x_datetime, "Value": y})
    st.line_chart(df.set_index("Date"))

# Excel保存セクション
st.subheader("💾 データ保存")
if "datasets" not in st.session_state:
    st.session_state.datasets = {}

if st.button("✅ このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("📥 Excel に全データを書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(
        label="📂 ダウンロード",
        data=output.getvalue(),
        file_name="synthetic_timeseries.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

assistant:

素晴らしい改良ですね!
GUIがコンパクトになり、ユーザーがパラメータを直感的に操作しながら、リアルタイムにグラフの変化を確認できるようになっていて、使い勝手がとても良さそうです。

さらにブラッシュアップしたい場合、例えば:

💡 改良アイデア(必要なら)

  1. 保存済みデータ一覧の表示

    if st.session_state.datasets:
        st.markdown("### 💼 保存済みデータ一覧")
        st.write(list(st.session_state.datasets.keys()))
    
  2. グラフの色やスタイル選択(Optional)
    シンプルな色選択を追加しても面白いです。

  3. 移動平均線を表示(Optional)

    show_ma = st.checkbox("移動平均線を表示")
    if show_ma:
        window = st.slider("移動平均の期間(日)", 2, 30, 7)
        df["MA"] = df["Value"].rolling(window=window).mean()
        st.line_chart(df.set_index("Date")[["Value", "MA"]])
    

必要であれば上記も組み込みます!
他にも「保存ファイル名を自動で日付付きにする」とか「エクセル出力時にメタ情報(パラメータ)も書き出す」などの拡張もできますよー。

何か加えたい機能、調整したい表示ありますか?

user:

ありがとうございます。保存済みデータ一覧の表示をお願いします。あと、日次を月次や年次に変更しようとしたときに次のエラーが出るので解消して欲しいです。

streamlit.errors.StreamlitValueAboveMaxError: The value 200.0 is greater than the max_value 5.0.

Traceback:
File "/Users/maskot/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/maskot/streamlit-app/venv/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 640, in code_to_exec
    exec(code, module.__dict__)
File "/Users/maskot/streamlit-app/app.py", line 67, in <module>
    x0 = cols[2].number_input(f"変曲点{i+1}", 0.0, float(len(x_numeric)), float((i + 1) * 200), 1.0, key=f"x0{i}")
File "/Users/maskot/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/maskot/streamlit-app/venv/lib/python3.10/site-packages/streamlit/elements/widgets/number_input.py", line 324, in number_input
    return self._number_input(
File "/Users/maskot/streamlit-app/venv/lib/python3.10/site-packages/streamlit/elements/widgets/number_input.py", line 460, in _number_input
    raise StreamlitValueAboveMaxError(value=value, max_value=max_value)

assistant:

ありがとうございます!以下の2点を対応しますね:


1. エラー修正:StreamlitValueAboveMaxError

エラーの原因は、number_input の初期値 value=(i+1)*200 が、max_value=float(len(x_numeric)) よりも大きくなってしまうことにあります。

🔧 対処法:

初期値を max_value を超えない範囲で動的に制御します:

x0_max = float(len(x_numeric))
x0_default = min((i + 1) * 200, x0_max)
x0 = cols[2].number_input(
    f"変曲点{i+1}", 0.0, x0_max, x0_default, 1.0, key=f"x0{i}"
)

2. 保存済みデータ一覧の表示

サイドバーまたはメイン画面に保存されたデータのシート名一覧を表示できます。

以下では サイドバー に追加しています:

# 保存済みデータ一覧
with st.sidebar:
    if "datasets" in st.session_state and st.session_state.datasets:
        st.markdown("### 💾 保存済みデータ一覧")
        for name in st.session_state.datasets.keys():
            st.write(f"📄 {name}")

🧩 修正済み該当部分の統合コード(差分のみ)

# x0入力部分(エラー修正)
x0_max = float(len(x_numeric))
x0_default = min((i + 1) * 200, x0_max)
x0 = cols[2].number_input(
    f"変曲点{i+1}",
    0.0,
    x0_max,
    x0_default,
    1.0,
    key=f"x0{i}",
)
# サイドバーに保存済みデータ一覧
with st.sidebar:
    if "datasets" in st.session_state and st.session_state.datasets:
        st.markdown("### 💾 保存済みデータ一覧")
        for name in st.session_state.datasets.keys():
            st.write(f"📄 {name}")

必要でしたら、これを組み込んだ全体コードを再掲することもできますので、お気軽にどうぞ!他にも気になる点があれば何でもおっしゃってください。

user:

ありがとうございます。全体コードを再掲してください。

assistant:

こちらが、保存済みデータ一覧の表示機能と、変曲点のエラー修正を含んだ最新の 全体コード です👇

from io import BytesIO
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import streamlit as st

# データ生成関数
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)

def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(
        amplitudes[:, None]
        * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]),
        axis=0,
    )

# ランダムウォークノイズ
def generate_random_walk(length, std_dev):
    noise = np.random.normal(loc=0, scale=std_dev, size=length)
    return np.cumsum(noise)

# Streamlit UI
st.set_page_config(layout="wide")
st.title("🧪 人工時系列データ合成アプリ")

# 左サイドバー:全体パラメータ
with st.sidebar:
    st.header("🔧 全体設定")
    freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
    years = st.number_input("何年分のデータを生成しますか?", min_value=1, max_value=100, value=3)
    start_date = st.date_input("開始日", value=pd.to_datetime("2020-01-01"))
    save_name = st.text_input("保存名(Excelシート名)", value="dataset1")
    noise_std = st.slider("ノイズの大きさ(標準偏差)", 0.0, 5.0, 0.0, step=0.1)

    # 保存済みデータ一覧
    if "datasets" in st.session_state and st.session_state.datasets:
        st.markdown("### 💾 保存済みデータ一覧")
        for name in st.session_state.datasets.keys():
            st.write(f"📄 {name}")

# 日付・x軸の生成
if freq == "日次":
    dates = pd.date_range(start=start_date, periods=years * 365, freq="D")
elif freq == "月次":
    dates = pd.date_range(start=start_date, periods=years * 12, freq="M")
else:
    dates = pd.date_range(start=start_date, periods=years, freq="Y")

x_numeric = np.linspace(0, len(dates), len(dates))
x_datetime = dates

# メインレイアウト
col_graph, col_params = st.columns([2.5, 1.5])

with col_params:
    st.subheader("📈 関数パラメータ")

    # シグモイド関数
    with st.expander("シグモイド関数"):
        num_sigmoid = st.number_input("", 1, 5, 3, key="sigmoid_count")
        sigmoid_params = []
        for i in range(num_sigmoid):
            st.markdown(f"**Sigmoid #{i+1}**")
            cols = st.columns(3)
            B = cols[0].number_input(
                f"スケール{i+1}", -10.0, 10.0, float(i + 1), 0.1, key=f"B{i}"
            )
            C = cols[1].number_input(
                f"傾き{i+1}", -5.0, 5.0, float(0.3 + (i - 0.5)), 0.1, key=f"C{i}"
            )
            x0_max = float(len(x_numeric))
            x0_default = min((i + 1) * 200, x0_max)
            x0 = cols[2].number_input(
                f"変曲点{i+1}", 0.0, x0_max, x0_default, 1.0, key=f"x0{i}"
            )
            sigmoid_params.append([B, C, x0])
        A_sigmoid = np.array(sigmoid_params)

    # サイン関数
    with st.expander("サイン関数"):
        num_sin = st.number_input("", 1, 5, 3, key="sin_count")
        sin_params = []
        freqs = [7.0, 31.0, 365.1, 29.1]
        for i in range(num_sin):
            st.markdown(f"**Sin #{i+1}**")
            cols = st.columns(3)
            A = cols[0].number_input(
                f"振幅{i+1}", 0.0, 10.0, float(i + 1), 0.1, key=f"A{i}"
            )
            P = cols[1].number_input(
                f"周期{i+1}", 1.0, float(len(x_numeric)), freqs[i % len(freqs)], 1.0, key=f"P{i}"
            )
            phase = cols[2].number_input(
                f"位相{i+1}", 0.0, 2 * np.pi, float(i + 1), 0.1, key=f"phase{i}"
            )
            sin_params.append([A, P, phase])
        A_sin = np.array(sin_params)

# グラフ描画
with col_graph:
    y = sum_of_sigmoids(A_sigmoid, x_numeric) + sum_of_sin(A_sin, x_numeric)

    if noise_std > 0:
        noise = generate_random_walk(len(y), noise_std)
        y += noise

    df = pd.DataFrame({"Date": x_datetime, "Value": y})
    st.line_chart(df.set_index("Date"))

# Excel保存セクション
st.subheader("💾 データ保存")
if "datasets" not in st.session_state:
    st.session_state.datasets = {}

if st.button("✅ このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("📥 Excel に全データを書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(
        label="📂 ダウンロード",
        data=output.getvalue(),
        file_name="synthetic_timeseries.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

✅ 主な改善点まとめ

  • 変曲点入力時の最大値制御:エラー防止のため min(..., max_value) を導入
  • 保存済みデータ一覧表示st.sidebardatasets の一覧を表示

他にも「UIをもっと見やすくしたい」「保存前にプレビュー確認したい」などあれば、遠慮なくどうぞ!

user:

ありがとうございます。次のエラーになりましたので、対処お願いします。

streamlit.errors.StreamlitMixedNumericTypesError: All numerical arguments must be of the same type. value has int type. max_value has float type. step has float type.

Traceback:
File "/Users/maskot/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/maskot/streamlit-app/venv/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 640, in code_to_exec
    exec(code, module.__dict__)
File "/Users/maskot/streamlit-app/app.py", line 83, in <module>
    x0 = cols[2].number_input(
File "/Users/maskot/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/maskot/streamlit-app/venv/lib/python3.10/site-packages/streamlit/elements/widgets/number_input.py", line 324, in number_input
    return self._number_input(
File "/Users/maskot/streamlit-app/venv/lib/python3.10/site-packages/streamlit/elements/widgets/number_input.py", line 398, in _number_input
    raise StreamlitMixedNumericTypesError(value=value, min_value=min_value, max_value=max_value, step=step)

assistant:

このエラーは、st.number_input に渡している引数の 数値型が混在している(int と float を混ぜて使っている) ために発生しています。

🔧 解決方法

value, min_value, max_value, step をすべて 同じ型(今回は float に統一すれば OK です。


✅ 修正する該当箇所(変曲点の入力)

x0_max = float(len(x_numeric))
x0_default = float(min((i + 1) * 200, x0_max))  # int → float に変換
x0 = cols[2].number_input(
    f"変曲点{i+1}",
    float(0.0),  # 明示的に float
    x0_max,
    x0_default,
    float(1.0),  # ここも float に
    key=f"x0{i}"
)

✨ 修正後の該当ブロック(再掲)

以下を現在のコードと置き換えてください:

x0_max = float(len(x_numeric))
x0_default = float(min((i + 1) * 200, x0_max))
x0 = cols[2].number_input(
    f"変曲点{i+1}",
    float(0.0),
    x0_max,
    x0_default,
    float(1.0),
    key=f"x0{i}"
)

これで StreamlitMixedNumericTypesError は解消されます!
他にも詰まりそうな箇所があれば、気軽に聞いてくださいね 🙌

user:

ありがとうございます。月次にすると

streamlit.errors.StreamlitValueAboveMaxError: The value 31.0 is greater than the max_value 24.0.
と言われ、年次にするとstreamlit.errors.StreamlitValueAboveMaxError: The value 7.0 is greater than the max_value 2.0.

と言われてしまいます。修正点をステップバイステップで考えて直していただけませんか。

user:

はい、ありがとうございます!エラーをしっかり確認して、ステップバイステップで丁寧に原因を追って修正していきましょう。


❗ エラーの原因

Streamlit の number_input において、default value(初期値)が max_value を超えてしまっている、という典型的なエラーです:

The value X is greater than the max_value Y

🔍 どこで起きているのか?

1. サイン関数の「周期 P」のデフォルト値に関係しています:

freqs = [7.0, 31.0, 365.1, 29.1]  # 固定の値を周期に指定している

これは「日次」用に作られた値で、月次(データ数 ≈ 12 × 年数)、年次(データ数 ≈ 年数)に切り替えると、len(x_numeric) が小さくなり、max_value を超えてしまいます。


✅ 修正ステップ(順を追って)

✅ ステップ 1:サイン関数の周期候補をデータ長さに合わせて自動調整する

# 周期の候補を動的に設定
if freq == "日次":
    freqs = [7.0, 31.0, 365.1, 29.1]
elif freq == "月次":
    freqs = [3.0, 6.0, 12.0]  # 月単位の周期
else:
    freqs = [1.0, 2.0]  # 年単位の周期

✅ ステップ 2:周期の max_valuelen(x_numeric) に合わせて動的に設定する

max_period = float(len(x_numeric))

✅ ステップ 3:「周期 P」の入力欄で、デフォルト値が max_value を超えないように調整

P_default = float(min(freqs[i % len(freqs)], max_period))
P = cols[1].number_input(
    f"周期{i+1}",
    min_value=1.0,
    max_value=max_period,
    value=P_default,
    step=1.0,
    key=f"P{i}"
)

💡 まとめ:調整ポイントの考え方

調整対象 内容
周期候補 freqs 粒度(日・月・年)ごとに適したリストを用意
max_value len(x_numeric) に連動して動的に決定する
default value freqs[i]max_value を比較して小さい方を使う

🚀 次にやること

この修正ステップを、該当コードブロックに適用してみてください。
うまくいかなかったり、コード全体で見たい場合は、そこまで含めて再構成してお渡しします!

どうしますか?コード全体を再掲しましょうか?

user:

ありがとうございます。コード全体を再掲してください。

assistant:

以下に修正済みのコード全体を再掲します。主要な修正点としては、周期のデフォルト値を動的に調整し、エラーを回避できるようにしました。

from io import BytesIO
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import streamlit as st


# データ生成関数
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)


def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(
        amplitudes[:, None]
        * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]),
        axis=0,
    )


# ランダムウォークノイズ
def generate_random_walk(length, std_dev):
    noise = np.random.normal(loc=0, scale=std_dev, size=length)
    return np.cumsum(noise)


# Streamlit UI
st.set_page_config(layout="wide")
st.title("🧪 人工時系列データ合成アプリ")

# 左サイドバー:全体パラメータ
with st.sidebar:
    st.header("🔧 全体設定")
    freq = st.selectbox("時系列の粒度", ["日次", "月次", "年次"])
    years = st.number_input(
        "何年分のデータを生成しますか?", min_value=1, max_value=100, value=3
    )
    start_date = st.date_input("開始日", value=pd.to_datetime("2020-01-01"))
    save_name = st.text_input("保存名(Excelシート名)", value="dataset1")
    noise_std = st.slider("ノイズの大きさ(標準偏差)", 0.0, 5.0, 0.0, step=0.1)

# 日付・x軸の生成
if freq == "日次":
    dates = pd.date_range(start=start_date, periods=years * 365, freq="D")
elif freq == "月次":
    dates = pd.date_range(start=start_date, periods=years * 12, freq="M")
else:
    dates = pd.date_range(start=start_date, periods=years, freq="Y")

x_numeric = np.linspace(0, len(dates), len(dates))
x_datetime = dates

# メインレイアウト
col_graph, col_params = st.columns([2.5, 1.5])

with col_params:
    st.subheader("📈 関数パラメータ")

    # シグモイド
    with st.expander("シグモイド関数"):
        num_sigmoid = st.number_input("", 1, 5, 3, key="sigmoid_count")
        sigmoid_params = []
        for i in range(num_sigmoid):
            st.markdown(f"**Sigmoid #{i+1}**")
            cols = st.columns(3)
            B = cols[0].number_input(
                f"スケール{i+1}", -10.0, 10.0, float(i + 1), 0.1, key=f"B{i}"
            )
            C = cols[1].number_input(
                f"傾き{i+1}", -5.0, 5.0, float(0.3 + (i - 0.5)), 0.1, key=f"C{i}"
            )
            x0 = cols[2].number_input(
                f"変曲点{i+1}",
                0.0,
                float(len(x_numeric)),
                float((i + 1) * 200),
                1.0,
                key=f"x0{i}",
            )
            sigmoid_params.append([B, C, x0])
        A_sigmoid = np.array(sigmoid_params)

    # サイン
    with st.expander("サイン関数"):
        num_sin = st.number_input("", 1, 5, 3, key="sin_count")
        sin_params = []
        
        # 粒度に応じて周期候補を設定
        if freq == "日次":
            freqs = [7.0, 31.0, 365.1, 29.1]
        elif freq == "月次":
            freqs = [3.0, 6.0, 12.0]  # 月単位の周期
        else:
            freqs = [1.0, 2.0]  # 年単位の周期

        for i in range(num_sin):
            st.markdown(f"**Sin #{i+1}**")
            cols = st.columns(3)
            A = cols[0].number_input(
                f"振幅{i+1}", 0.0, 10.0, float(i + 1), 0.1, key=f"A{i}"
            )
            
            # max_valueはx_numericの長さに基づいて動的に設定
            max_period = float(len(x_numeric))
            P_default = float(min(freqs[i % len(freqs)], max_period))
            
            P = cols[1].number_input(
                f"周期{i+1}",
                min_value=1.0,
                max_value=max_period,
                value=P_default,
                step=1.0,
                key=f"P{i}"
            )
            
            phase = cols[2].number_input(
                f"位相{i+1}", 0.0, 2 * np.pi, float(i + 1), 0.1, key=f"phase{i}"
            )
            sin_params.append([A, P, phase])
        A_sin = np.array(sin_params)

# グラフ描画
with col_graph:
    y = sum_of_sigmoids(A_sigmoid, x_numeric) + sum_of_sin(A_sin, x_numeric)

    if noise_std > 0:
        noise = generate_random_walk(len(y), noise_std)
        y += noise

    df = pd.DataFrame({"Date": x_datetime, "Value": y})
    st.line_chart(df.set_index("Date"))

# 保存済みデータ一覧
st.subheader("💾 保存済みデータ")
if "datasets" not in st.session_state:
    st.session_state.datasets = {}

for sheet_name in st.session_state.datasets:
    st.write(f"- {sheet_name}")

# Excel保存セクション
st.subheader("💾 データ保存")
if st.button("✅ このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("📥 Excel に全データを書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(
        label="📂 ダウンロード",
        data=output.getvalue(),
        file_name="synthetic_timeseries.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

修正ポイント:

  1. 周期候補 freqs の動的変更:

    • 日次 → [7.0, 31.0, 365.1, 29.1]
    • 月次 → [3.0, 6.0, 12.0]
    • 年次 → [1.0, 2.0]
  2. max_value の調整:

    • len(x_numeric) に基づいて max_value を動的に設定。
  3. 保存済みデータの一覧表示:

    • すべての保存データ名を表示。

これで、月次や年次に切り替えた際にエラーが発生しないはずです。データの保存とダウンロードも確認できます。

もし他に質問があれば、気軽にお知らせください!

user:

ありがとうございます。月次や年次に切り替えた際のエラーは解消しました。エクセルを保存するときの次のエラーを解消していただけませんか。

ModuleNotFoundError: No module named 'xlsxwriter'
Traceback:
File "/Users/maskot/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/maskot/streamlit-app/venv/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 640, in code_to_exec
    exec(code, module.__dict__)
File "/Users/maskot/streamlit-app/app.py", line 157, in <module>
    with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
File "/Users/maskot/streamlit-app/venv/lib/python3.10/site-packages/pandas/io/excel/_xlsxwriter.py", line 197, in __init__
    from xlsxwriter import Workbook

assistant:

このエラーは、xlsxwriter パッケージが現在の仮想環境にインストールされていないために発生しています。pandas.ExcelWriter(engine="xlsxwriter") を使用するには、xlsxwriter を別途インストールする必要があります。


✅ 解決方法(ステップバイステップ)

🔧 1. ターミナルで仮想環境を有効にする

あなたの Streamlit アプリが動作している仮想環境(おそらく venv)を有効にします:

source /Users/maskot/streamlit-app/venv/bin/activate

💡 2. xlsxwriter をインストールする

以下のコマンドを実行してください:

pip install xlsxwriter

🔁 3. アプリを再起動

インストールが完了したら、Streamlit アプリを再起動します:

streamlit run app.py

🔄 別解(推奨)

xlsxwriter を使わず、openpyxl(pandas に標準的に含まれている場合が多い)を使うこともできます:

with pd.ExcelWriter(output, engine="openpyxl") as writer:
    ...

こちらの方が追加インストール不要で済む可能性があります。


💡 補足

仮想環境で使っているパッケージの一覧を確認したい場合は:

pip list

どちらの方法でも大丈夫ですが、xlsxwriter をインストールすれば今のコードのままで動きます。openpyxl に変更する方が手軽なこともあります。ご希望あればコード変更のサポートもします!

user:

ありがとうございます。現在のコードをお伝えします。openpyxlを用いたコードに変更して、全コードを書いてください。

from io import BytesIO

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import streamlit as st


# データ生成関数
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)


def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(
        amplitudes[:, None]
        * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]),
        axis=0,
    )


# ランダムウォークノイズ
def generate_random_walk(length, std_dev):
    noise = np.random.normal(loc=0, scale=std_dev, size=length)
    return np.cumsum(noise)


# Streamlit UI
st.set_page_config(layout="wide")
st.title("🧪 人工時系列データ合成アプリ")

# 左サイドバー:全体パラメータ
with st.sidebar:
    st.header("🔧 全体設定")
    freq = st.selectbox("時系列の粒度", ["月次", "年次", "日次"])
    years = st.number_input(
        "何年分のデータを生成しますか?", min_value=1, max_value=100, value=10
    )
    start_date = st.date_input("開始日", value=pd.to_datetime("2020-01-01"))
    save_name = st.text_input("保存名(Excelシート名)", value="dataset1")
    noise_std = st.slider("ノイズの大きさ(標準偏差)", 0.0, 5.0, 0.0, step=0.1)

# 日付・x軸の生成
if freq == "日次":
    dates = pd.date_range(start=start_date, periods=years * 365, freq="D")
elif freq == "月次":
    dates = pd.date_range(start=start_date, periods=years * 12, freq="M")
else:
    dates = pd.date_range(start=start_date, periods=years, freq="Y")

x_numeric = np.linspace(0, len(dates), len(dates))
x_datetime = dates

# メインレイアウト
col_graph, col_params = st.columns([2.5, 1.5])

with col_params:
    st.subheader("📈 関数パラメータ")

    # シグモイド
    with st.expander("シグモイド関数"):
        num_sigmoid = st.number_input("", 1, 5, 3, key="sigmoid_count")
        sigmoid_params = []
        for i in range(num_sigmoid):
            st.markdown(f"**Sigmoid #{i+1}**")
            cols = st.columns(3)
            B = cols[0].number_input(
                f"スケール{i+1}", -10.0, 10.0, float(i + 1), 0.1, key=f"B{i}"
            )
            C = cols[1].number_input(
                f"傾き{i+1}", -5.0, 5.0, float(0.3 + (i - 0.5)), 0.1, key=f"C{i}"
            )
            x0 = cols[2].number_input(
                f"変曲点{i+1}",
                0.0,
                float(len(x_numeric)),
                float(((i + 1) * 200) % len(x_numeric)),
                1.0,
                key=f"x0{i}",
            )
            sigmoid_params.append([B, C, x0])
        A_sigmoid = np.array(sigmoid_params)

    # サイン
    with st.expander("サイン関数"):
        num_sin = st.number_input("", 1, 5, 3, key="sin_count")
        sin_params = []

        # 粒度に応じて周期候補を設定
        if freq == "日次":
            freqs = [7.0, 31.0, 365.1, 29.1]
        elif freq == "月次":
            freqs = [3.0, 6.0, 12.0]  # 月単位の周期
        else:
            freqs = [1.0, 2.0]  # 年単位の周期

        for i in range(num_sin):
            st.markdown(f"**Sin #{i+1}**")
            cols = st.columns(3)
            A = cols[0].number_input(
                f"振幅{i+1}", 0.0, 10.0, float(i + 1), 0.1, key=f"A{i}"
            )

            # max_valueはx_numericの長さに基づいて動的に設定
            max_period = float(len(x_numeric))
            P_default = float(min(freqs[i % len(freqs)], max_period))

            P = cols[1].number_input(
                f"周期{i+1}",
                min_value=1.0,
                max_value=max_period,
                value=P_default,
                step=1.0,
                key=f"P{i}",
            )

            phase = cols[2].number_input(
                f"位相{i+1}", 0.0, 2 * np.pi, float(i + 1), 0.1, key=f"phase{i}"
            )
            sin_params.append([A, P, phase])
        A_sin = np.array(sin_params)

# グラフ描画
with col_graph:
    y = sum_of_sigmoids(A_sigmoid, x_numeric) + sum_of_sin(A_sin, x_numeric)

    if noise_std > 0:
        noise = generate_random_walk(len(y), noise_std)
        y += noise

    df = pd.DataFrame({"Date": x_datetime, "Value": y})
    st.line_chart(df.set_index("Date"))

# 保存済みデータ一覧
st.subheader("💾 保存済みデータ")
if "datasets" not in st.session_state:
    st.session_state.datasets = {}

for sheet_name in st.session_state.datasets:
    st.write(f"- {sheet_name}")

# Excel保存セクション
st.subheader("💾 データ保存")
if st.button("✅ このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("📥 Excel に全データを書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(
        label="📂 ダウンロード",
        data=output.getvalue(),
        file_name="synthetic_timeseries.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

assistant:

ありがとうございます。以下はご要望に応じて xlsxwriteropenpyxl に変更した 修正済みの全コードです。他のロジックはそのままで、Excel出力部分だけを変更しています。


✅ 修正ポイント

  • pd.ExcelWriter(output, engine="xlsxwriter")pd.ExcelWriter(output, engine="openpyxl")

📝 修正後のコード全文(openpyxl使用)

from io import BytesIO

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import streamlit as st


# データ生成関数
def sum_of_sigmoids(A, x):
    A = np.asarray(A)
    B = A[:, 0]
    C = A[:, 1]
    x_inflection = A[:, 2]
    D = -C * x_inflection
    return np.sum(B[:, None] / (1 + np.exp(-(C[:, None] * x + D[:, None]))), axis=0)


def sum_of_sin(A, x):
    A = np.asarray(A)
    amplitudes = A[:, 0]
    periods = A[:, 1]
    phases = A[:, 2]
    return np.sum(
        amplitudes[:, None]
        * np.sin(2 * np.pi * x / periods[:, None] + phases[:, None]),
        axis=0,
    )


# ランダムウォークノイズ
def generate_random_walk(length, std_dev):
    noise = np.random.normal(loc=0, scale=std_dev, size=length)
    return np.cumsum(noise)


# Streamlit UI
st.set_page_config(layout="wide")
st.title("🧪 人工時系列データ合成アプリ")

# 左サイドバー:全体パラメータ
with st.sidebar:
    st.header("🔧 全体設定")
    freq = st.selectbox("時系列の粒度", ["月次", "年次", "日次"])
    years = st.number_input(
        "何年分のデータを生成しますか?", min_value=1, max_value=100, value=10
    )
    start_date = st.date_input("開始日", value=pd.to_datetime("2020-01-01"))
    save_name = st.text_input("保存名(Excelシート名)", value="dataset1")
    noise_std = st.slider("ノイズの大きさ(標準偏差)", 0.0, 5.0, 0.0, step=0.1)

# 日付・x軸の生成
if freq == "日次":
    dates = pd.date_range(start=start_date, periods=years * 365, freq="D")
elif freq == "月次":
    dates = pd.date_range(start=start_date, periods=years * 12, freq="M")
else:
    dates = pd.date_range(start=start_date, periods=years, freq="Y")

x_numeric = np.linspace(0, len(dates), len(dates))
x_datetime = dates

# メインレイアウト
col_graph, col_params = st.columns([2.5, 1.5])

with col_params:
    st.subheader("📈 関数パラメータ")

    # シグモイド
    with st.expander("シグモイド関数"):
        num_sigmoid = st.number_input("", 1, 5, 3, key="sigmoid_count")
        sigmoid_params = []
        for i in range(num_sigmoid):
            st.markdown(f"**Sigmoid #{i+1}**")
            cols = st.columns(3)
            B = cols[0].number_input(
                f"スケール{i+1}", -10.0, 10.0, float(i + 1), 0.1, key=f"B{i}"
            )
            C = cols[1].number_input(
                f"傾き{i+1}", -5.0, 5.0, float(0.3 + (i - 0.5)), 0.1, key=f"C{i}"
            )
            x0 = cols[2].number_input(
                f"変曲点{i+1}",
                0.0,
                float(len(x_numeric)),
                float(((i + 1) * 200) % len(x_numeric)),
                1.0,
                key=f"x0{i}",
            )
            sigmoid_params.append([B, C, x0])
        A_sigmoid = np.array(sigmoid_params)

    # サイン
    with st.expander("サイン関数"):
        num_sin = st.number_input("", 1, 5, 3, key="sin_count")
        sin_params = []

        if freq == "日次":
            freqs = [7.0, 31.0, 365.1, 29.1]
        elif freq == "月次":
            freqs = [3.0, 6.0, 12.0]
        else:
            freqs = [1.0, 2.0]

        for i in range(num_sin):
            st.markdown(f"**Sin #{i+1}**")
            cols = st.columns(3)
            A = cols[0].number_input(
                f"振幅{i+1}", 0.0, 10.0, float(i + 1), 0.1, key=f"A{i}"
            )

            max_period = float(len(x_numeric))
            P_default = float(min(freqs[i % len(freqs)], max_period))

            P = cols[1].number_input(
                f"周期{i+1}",
                min_value=1.0,
                max_value=max_period,
                value=P_default,
                step=1.0,
                key=f"P{i}",
            )

            phase = cols[2].number_input(
                f"位相{i+1}", 0.0, 2 * np.pi, float(i + 1), 0.1, key=f"phase{i}"
            )
            sin_params.append([A, P, phase])
        A_sin = np.array(sin_params)

# グラフ描画
with col_graph:
    y = sum_of_sigmoids(A_sigmoid, x_numeric) + sum_of_sin(A_sin, x_numeric)

    if noise_std > 0:
        noise = generate_random_walk(len(y), noise_std)
        y += noise

    df = pd.DataFrame({"Date": x_datetime, "Value": y})
    st.line_chart(df.set_index("Date"))

# 保存済みデータ一覧
st.subheader("💾 保存済みデータ")
if "datasets" not in st.session_state:
    st.session_state.datasets = {}

for sheet_name in st.session_state.datasets:
    st.write(f"- {sheet_name}")

# Excel保存セクション
st.subheader("💾 データ保存")
if st.button("✅ このデータを保存する"):
    st.session_state.datasets[save_name] = df
    st.success(f"{save_name} を保存しました。")

if st.button("📥 Excel に全データを書き出す"):
    output = BytesIO()
    with pd.ExcelWriter(output, engine="openpyxl") as writer:
        for sheet_name, data in st.session_state.datasets.items():
            data.to_excel(writer, index=False, sheet_name=sheet_name)
    st.download_button(
        label="📂 ダウンロード",
        data=output.getvalue(),
        file_name="synthetic_timeseries.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )

これで openpyxl を使った保存が可能になります。なお、openpyxl が入っていない場合は次のコマンドでインストールしてください:

pip install openpyxl

他に気になるところがあればいつでも教えてください!

user:

ありがとうございました!ブログに記事を書きたいのでタイトル案を列挙していただけますか?

assistant:

もちろんです!このStreamlitアプリの内容をもとに、ブログ記事のタイトル案をいくつかジャンル別にご提案しますね。


💡 一般向け・読みやすさ重視

  1. ノーコード感覚で時系列データを合成!Streamlitで作る可視化アプリ
  2. データ分析のための合成時系列を簡単生成!Python × Streamlit の活用例
  3. シグモイドとサインで遊ぶ!人工データを直感的に作れるWebアプリの紹介
  4. 研究や教育に便利!時系列データを自在に作れるPythonアプリ
  5. 手軽にExcel出力も!Streamlitで作る時系列シミュレーションツール

🧪 テクニカル・開発者向け

  1. Streamlitでつくる人工時系列データ合成アプリ:シグモイド+サイン+ノイズ
  2. Pythonとopenpyxlで時系列データをExcelに書き出すWebアプリを構築する
  3. パラメータ操作で直感的に!合成時系列データのインタラクティブ可視化
  4. 研究・分析用の模擬データ生成ツールをStreamlitで作ってみた話
  5. Streamlit × NumPy × Pandasで構築する時系列データ合成アプリ

📊 応用・事例紹介風

  1. Excel出力にも対応!業務データのプロトタイピングに使えるStreamlitアプリ
  2. 機械学習の前処理にも最適?Streamlitで自作した時系列合成ツールのご紹介
  3. 時系列パターンの教材づくりにも!Pythonで作るデータ生成アプリの使い方
  4. “作ってみた”系:分析実験のための時系列データを直感操作で生成するWebアプリ

ご希望に応じて、記事の構成案や本文の執筆もお手伝いできますよ!
どの方向性が気になりますか?またはもう少し遊び心のあるタイトルも欲しいですか?

user:

「時系列データを自在に作れるStreamlitアプリ」にしようと思います。今回のまとめを書いてください。

assistant:

いいタイトルですね!「時系列データを自在に作れるStreamlitアプリ」にぴったり合う、ブログ記事の締めや概要文として使えるまとめ文を以下にご用意しました。


📝 まとめ:「時系列データを自在に作れるStreamlitアプリ」

本記事では、シグモイド関数・サイン関数・ランダムウォークノイズを組み合わせて、多様な時系列パターンを合成できるWebアプリの構築方法をご紹介しました。
このアプリは、Python × Streamlit の力を活かし、以下のような機能をシンプルなUIで提供します。

  • 粒度(年次・月次・日次)を指定して時系列を生成
  • 関数のパラメータをスライダーで自由に操作
  • 視覚的な確認とグラフ描画
  • 複数パターンをセッションに保存
  • ワンクリックでExcelファイル(複数シート)としてダウンロード可能

研究用の模擬データや、機械学習モデルのテストデータ、あるいは教材としての活用など、幅広い場面で役立つツールになっています。

「こういうデータが欲しかった!」という方にとって、試行錯誤しながら直感的に調整できる環境は非常に便利です。
Streamlit の柔軟性と、Python の数値処理の強さを活かしたこのアプリ、ぜひあなたのプロジェクトにも役立ててみてください!


ご希望があれば、この記事の導入文・本文構成案コード付きチュートリアル形式にもできますので、お気軽にご相談くださいね!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?