計算&グラフ可視化アプリ 開発中

【個人ブログ】開発過程と学習記録

投稿日

2~3週間前から個人的に大規模な分析アプリを開発しています。自分のプログラム歴に対してかなり挑戦的ですが何とか頑張って進めています。開発にはStreamlitというWebアプリ作成フレームワークを使っています。かなり使いやすくて関数もすぐ覚えれました。これと同時にPlotlyも初めて使ってみました。今まではmatplotlibとseabornのみでしたが、Streamlitとの相性良さではPlotlyが良いということで採用しました。ちなみにPlotlyの公式を見るとDashとが1番相性が良いらしいです。
このライブラリはアニメーションや3Dグラフにも対応しています。

この分析アプリは分析ごとの計算と同時にグラフを生成します。それに加えカスタマイズ機能も自由にできるようにしています。統計も学びながらなので時間はかかりそうです。AIの将来予測なども取り入れたり、Excel連携(API)も予定しています。ノーコードで誰でも分析ができて、可視化したグラフをカスタマイズして資料作りにも役立てたらと思っています。
現在もう少しで1ページが完成しそうなので一部コードを公開したいと思います。

初期化コード

if "graph_custom" is not st.session_state:
  st.session_state["graph_custom"] = {
    "customdata": None, "hovertemplate": None,
    "error_x": None, "error_y": None, 
    "graph_color": None, "mode": None, 
    "fill": None, "fillcolor": None, "fillpattern": None,
    "hovertemplate": None, "showlegend": True,
    "name": " ", "tickmode": "array",
    "x": None, "y": None, "xaxis": None, "yaxis": None,
    "xaxis": dict(tickvals=[None], ticktext=[None], title=dict(text="データ値(x)")),
    "yaxis": dict(tickvals=[None], ticktext=[None], title=dict(text="確率密度"))
  }

このst.session_stateを使った処理は最初は慣れなかったのですが、使う機会が多すぎて使いこなせるようになりました。これは初期化部分の一部で、レイアウトの部分がかなり多くなってますがおそらくまだ追加する予定です。実はこのレイアウトの選別がかなり時間かかっています。Plotlyのカスタマイズ機能が多すぎて公式ドキュメント見て自分で試しながらなので大変です。ChatGPTに聞いたりもしますが正確性が求められる分野なので、なるべく自分で調べてやっています。

グラフクラスと継承クラス

class CreateGraph:
  def __init__(self):
    self.frame = go.Figure(layout=dict(template="seaborn"))


class ScatterGraph(CreateGraph):
  def __init__(self, **kwargs):   
    super().__init__()
    self.options = kwargs
    self.frame.add_trace(go.Scatter(**kwargs))

ユーザーが入力したデータを代入してそのままPlotlyで使えるようにしています。テーマをseabornにしていますが反映されてるかよく分からないです。それを継承したクラスもここでは1つですが、複数作っています。親クラスではグラフのフレームを作り、この子クラスでは散布図のカスタマイズオプションを引数にしています。ちなみにPlotlyの公式のコード構造を参考に書きました。

グラフ関数(正規分布)

  def bellcurve(
      self, mu, sigma_or_s, n,
      *, alpha= None, lower = None, upper = None, org_datalist=None,
      **kwargs
  ):
    self.mu = mu
    self.sigma_or_s = sigma_or_s
    self.n = n
    self.alpha = alpha
    self.lower = lower
    self.upper = upper
    self.org_datalist = org_datalist
    self.vline = kwargs

    np.random.seed(42)
    
    n = judge_smp_size(n)
    smp_data = np.random.normal(mu, sigma_or_s, n)
    

    x = np.linspace(min(smp_data), max(smp_data), n)
    y = norm.pdf(x, mu, sigma_or_s)

    # 初期カスタマイズ
    if kwargs.get("hovertemplate") is None:
      mu_array = np.full_like(x, mu)
      sigma_array = np.full_like(x, sigma_or_s)
      customdata = np.stack([mu_array, sigma_array, x, y], axis=-1)
      hovertemplate = (
        "平均値: %{customdata[0]:.2f}<br>" +
        "偏差: %{customdata[1]:.2f}<br>" +
        "データ値: %{customdata[2]:.2f}<br>" +
        "確率密度: %{customdata[3]:.4f}<extra></extra>"
      )
      self.options.update(
        {"customdata": customdata, "hovertemplate": hovertemplate}
      )

    
    self.options.update({"x": x, "y": y})
    self.frame.add_trace(go.Scatter(**self.options))

    # 初期カスタマイズ
    if self.vline:
      self.vline.update({"x": lower, "annotation_text": f"{alpha}%"})
      self.frame.add_vline(**self.vline)

      self.vline.update({"x": upper, "annotation_position": "top right"})
      self.frame.add_vline(**self.vline)

      self.vline.update(
        {
          "x": mu, "annotation_text": mu, "annotation_position": "top",
          "legendgroup": None, "legendgrouptitle_text": "平均値",
          "line": dict(color=None, dash=None, width=2),
        }
      )
      self.frame.add_vline(**self.vline)

    st.session_state["ci_graphdata"] = self.frame

    return self.frame 

現在取り掛かってるのは信頼区間なんですが、正規分布が主に使われています。ここではカスタマイズの初期設定をしています。グラフは、最初はこちらで決めたこの正規分布とカスタマイズで生成するようにしていて、その後ユーザーが自由にカスタマイズできる感じになっています。区間範囲を表すvlineもここで生成しています。

計算関数(信頼区間)

def ci_cal(*args):

# 元データなし
  if len(args) == 6:
    sel_mean, sel_ht, mu, sigma_or_s, n, percent = args
    alpha = 1 - percent / 100
    df = n - 1

    ratio = get_ratio(sel_mean, sel_ht, alpha, df)

    lower = mu - ratio * (sigma_or_s / np.sqrt(n))
    upper =  mu + ratio * (sigma_or_s / np.sqrt(n))     

    ci_calresult = (
      mu, sigma_or_s, n, percent, f"{alpha:.2f}", 
      round(lower, 2), round(upper, 2), None
    )

    return ci_calresult, f"{percent}%信頼区間は{lower:.2f} ~ {upper:.2f}"  
       
# 元データあり
  elif len(args) == 5:
      sel_mean, sel_ht, n, percent, org_datalist = args
      alpha = 1 - percent / 100
      df = n - 1

      mu = np.mean(org_datalist)

      if sel_mean =="母平均値":
        sigma_or_s = np.std(org_datalist)
      else:
        sigma_or_s = np.std(org_datalist, ddof=1)  

      ratio = get_ratio(sel_mean, sel_ht, alpha, df)

      lower = mu - ratio * (sigma_or_s / np.sqrt(n))
      upper = mu + ratio * (sigma_or_s / np.sqrt(n))

      ci_calresult = (
        mu, sigma_or_s, n, percent, f"{alpha:.2f}", 
        round(lower, 2), round(upper, 2), None
      )

      return ci_calresult, f"{percent}%信頼区間は{lower:.2f} ~ {upper:.2f}"

ユーザーが入力したデータをここで計算します。そのあと先ほどのグラフ関数に送ります。可変長変数で元データの有無で分岐します。元データが引数で渡される場合は、標準偏差や平均などをここで計算してから信頼区間を計算します。戻り値は2つありますが、1つは計算結果の出力欄に表示させるためと、もう1つはグラフに送るためです。

計算開始後の処理

この計算結果出力には、実は入力欄を使用しています。理由としては個人的なこだわりですが、placeholderで文字を表示させたかったからです。割りと序盤に作ったので、まだStreamlitの理解が十分ではなく悩みながら作りました。

  if start_ci:
    to_cal = ci_cal(*ci_args)
    cal_result = to_cal[1]   # 計算結果出力用

    st.session_state["ci_data"] = {
      "sel_mean": sel_mean, "sel_ht": sel_ht,
      "mu": to_cal[0][0], "sigma_or_s": to_cal[0][1],
      "n": to_cal[0][2], "percent": percent,  
      "orgdata": org_datalist,  
      "alpha": 1 - percent / 100
    }

    st.session_state["ci_data"] = {"lower": to_cal[0][5], "upper": to_cal[0][6]}

    graph_pos_args = to_cal[0][0], to_cal[0][1], to_cal[0][2]
    graph_kwargs = {
       "alpha": to_cal[0][4], "lower": to_cal[0][5], "upper": to_cal[0][6],
       "org_datalist": to_cal[0][7]
    }   
    graph = ScatterGraph()
    graph.bellcurve(*graph_pos_args, **graph_kwargs, **st.session_state["vline_custom"])
  else:
    cal_result = ""    # placeholderを表示させる
    st.write(cal_result)

with result_btn1:
  st.text_input(
      "小数点以下2桁で表示されます",
      key="result1", value=cal_result,
      placeholder="ここに結果が反映されます",
)

計算開始ボタンを押した後の処理で、計算関数の戻り値を変数に入れ、入力欄に表示させています。このplaceholderは入力欄にしかないオプションだったので、何とか考えて書きました。ボタンを押す前は空文字を入れてplaceholderが表示されるようにしています。これをしないとケイン関数の戻り値がずっと表示してしまうので。

他にもたくさん書いたのですが長くなりそうなのでここまでにします。ちなみに1ページですでに400文字を超えています。まだ完成してないのでまだ長くなる予定です。コードが長いので、依存関係の調整がとても大変で数えきれないほどのエラーが出ました。最初はグラフを表示させるのすら上手くいかず、1週間以上試行錯誤してようやくできた感じです。分析ごとにページを変えて作る予定で、グラフ生成専用ページも作ろうと思っています。複数のグラフを並べられるようにも対応する予定です。

正直1つのページにこんなに時間かけるつもりはありませんでしたが、書いてる途中に良いアイディアが出てきて書き直すなんてこともよくありました。ただ、最初が1番時間がかかるものなのでこれ以降はより速いペースで出来ると思っています。GUIも初めてでしたが思ったより簡単で覚えやすかったです。

完成したらコードはGithubにて公開する予定なので興味ある方は是非ご覧ください。ブログで進歩状況も掲載していきます。

最後まで読んでいただきありがとうございました!

コメント

タイトルとURLをコピーしました