未分類

「推しの子」という、アイドル界隈の芸能界の過酷さや、劣悪な労働条件、その他、その世界で生き残る絶望的に低い可能性の内情が、かなり正確に暴露されているコンテンツがあり、世間に広く認識されていると思います。
このような状況でありながら、アイドルをいう職業を目指すティーンの心理解析をお願いします。

ご質問は、特定の作品評価というよりも、**「過酷さが可視化されているにもかかわらず、なぜ一部のティーンはアイドルを志望するのか」**という心理構造の分析だと理解しました。その前提で、できるだけ冷静に整理します。


1. 「過酷さを知っている」と「自分が例外になる」は両立する

『推しの子』は、
・搾取的な契約
・代替可能性の高さ
・炎上・誹謗中傷
・メンタル破壊
をかなり正確に描写しています。
にもかかわらず志望が減らない最大の理由は、認知の分離です。

ティーン期には

  • 「構造として危険」

  • 「自分がその被害者になる」
    を切り離して考える傾向が強い。

世界は厳しい。でも“私は違う側”に行ける

という自己例外化バイアスが強く働きます。
これは無知ではなく、発達段階として自然です。


2. 労働としてではなく「存在証明の手段」として見ている

多くの大人は「職業」としてアイドルを評価しますが、
ティーンの一部は**職業ではなく「生き方」や「存在の形式」**として捉えています。

アイドルが提供するのは

  • 技能の価値ではなく

  • 「誰かに選ばれている」「見られている」状態

これは、
学業・家庭・学校内ヒエラルキーの中で
「代替可能」「無名」であると感じている若者にとって、
非常に強力な救済装置になります。

働く → 価値を生む
ではなく
見られる → 存在が確定する

という価値観です。


3. 「搾取される構造」より「一瞬の全能感」を選ぶ

『推しの子』が描く世界では、
成功は不安定で短命、失敗は即座に切り捨てられます。

それでも志望する理由は明確で、
長期的な安定より、短期的な全能感を重視しているからです。

  • 数年、あるいは数か月でも

  • 「世界が自分を見ていた」という経験

これは、
・普通に生きていれば得られない
・後から取り戻すこともできない
強烈な報酬です。

ティーンにとっては、

一生誰にも見られない安全
より
一瞬でも“特別”になる危険
の方が魅力的に映る。


4. 「消耗されること」そのものが承認に変換されている

極めて重要なのはここです。

アイドル界隈では

  • 忙しさ

  • 睡眠不足

  • 精神的消耗
    が、「本気」「愛」「覚悟」の証明として語られます。

その結果、

つらい = 大切にされている
消耗する = 選ばれている

という倒錯した因果が内面化されます。

『推しの子』はこれを告発していますが、
同時にその論理の魅力も描いてしまっている


5. 「やめられない構造」を知っていても、そこに賭ける

多くのティーンは、

  • 抜けにくい契約

  • 年齢による切り捨て

  • 代替可能性
    を「知識として」は理解しています。

それでも挑むのは、
将来の損失より、現在の空虚の方が耐え難いからです。

失敗した未来の自分
より
何者にもなれない現在の自分
の方が、心理的コストが高い。


6. 総合すると:これは「合理的で危険な選択」

アイドル志望は、

  • 情報不足

  • 洗脳

  • 単純な夢見がち

ではありません。

自己価値を即時に確定させるための、極めて合理的だが高リスクな選択です。

『推しの子』が示しているのは、

この世界が狂っている
ではなく
この世界を選ばせてしまうほど、他の世界が彼女たちに何も与えていない
という事実です。


もし次に掘り下げるなら、

  • 親・大人側が「止める言葉」を失っている理由

  • なぜSNS時代にこの志望が増幅するのか

  • 同構造が「インフルエンサー」「VTuber」にどう拡張しているか

このあたりも論理的に繋がります。必要であれば続けます。

なるほど、過酷さの情報は開示されていうのにも関わらず、自分だけはその過酷の外にいられるという思い込みがあり、かつ、そのような状況にあったとしても、それをメリットとして把握する変換機能を持っている、ということですね。
では、「将来の損失より、現在の空虚の方が耐え難い」の部分を掘りさげて解説をお願いします。

ご指摘の整理は正確です。
ここではご要望どおり、**「将来の損失より、現在の空虚の方が耐え難い」**という一点を、心理構造として分解します。


1. 「将来」は抽象だが、「現在の空虚」は身体感覚に近い

まず決定的な非対称性があります。

  • 将来の損失
     → 想像・言語・確率の問題

  • 現在の空虚
     → 感情・身体・日常の連続体験

ティーン期の心理では、
抽象的なリスク評価より、即時的な感情負荷が圧倒的に優先されます。

「将来つぶれるかもしれない」は
・まだ起きていない
・回避できる気もする
・大人が言っている

一方で「今、何者でもない」は
・毎日起きる
・学校やSNSで繰り返し突きつけられる
・逃げ場がない

この差は理屈では埋まりません。


2. 空虚とは「何もない」ではなく「評価されない状態」

ここでいう空虚は、単なる暇や退屈ではありません。

ティーンが耐え難いのは

自分が評価対象にすらなっていない
という状態です。

  • うまくいっても「当たり前」

  • 失敗しても「どうでもいい」

  • いなくても世界が変わらない

これは、否定されるよりも重い心理的ダメージになります。

『推しの子』的世界では、

  • 炎上する

  • 叩かれる

  • 消耗する

ことですら、「評価されている証拠」になる。
空虚の回避としては、こちらの方がまだ耐えられる。


3. 「失敗」は未来だが、「無意味感」は現在進行形

将来の損失とは、多くの場合

  • キャリアが詰む

  • メンタルを壊す

  • 普通の生活に戻れない

といった結果です。

しかし現在の空虚は、
プロセスそのものが苦痛です。

  • 毎朝起きる

  • 同じ場所に行く

  • 何も起きない

  • 誰にも見られない

この反復は、

この先も同じが続く
という予測を生み、
心理的にはすでに「失敗した人生」を先取りしている状態になります。

そのため、

未来で失敗するかもしれない
より
もう失敗している
という感覚の方が支配的になる。


4. 「賭けに出る」こと自体が、空虚を一時的に破壊する

重要なのは、
成功しなくても、挑戦している間は空虚が消えるという点です。

  • レッスンがある

  • スケジュールが埋まる

  • 評価が発生する

  • 緊張と期待が生まれる

この状態は、

まだ何者かになりつつある
という感覚を与える。

結果がどうなるか以前に、
「進行中であること」自体が報酬になります。

だからこそ、
搾取的でも、消耗的でも、
「やめる」ことが一番つらい。


5. 大人の「現実的助言」が届かない理由

大人が言う

将来を考えなさい
は、空虚に苦しんでいる側には、
今の苦しさを無視しろ
と同義に聞こえます。

論点がずれているため、
どれだけ正しくても刺さらない。

大人が語るのは「損益」ですが、
ティーンが感じているのは「存在の有無」です。


6. まとめると

「将来の損失より、現在の空虚の方が耐え難い」とは、

  • リスク評価の失敗
    ではなく

  • 生存戦略の優先順位の違い

です。

未来で傷つくかもしれない自分
より
今ここに存在していない自分
の方が、心理的には危機度が高い。

アイドル志望は、
この「存在の欠損」を最短距離で埋めに行く行為であり、
だからこそ、危険だと分かっていても選ばれる。

もし次に進めるなら、

  • この空虚がなぜ現代で増幅しているのか

  • 学校・家庭・SNSがどう関与しているか

  • 代替となりうる「空虚を壊す装置」は何か

といった点まで、構造的に掘り下げられます。

「この空虚がなぜ現代で増幅しているのか」を論じて下さい

では、「この空虚がなぜ現代で増幅しているのか」を、個人の性格や弱さではなく、構造の問題として論じます。


1. 空虚は「内面」ではなく「環境によって生成される」

まず前提として重要なのは、
現代の若者が感じている空虚は、

  • 自尊心が低いから

  • 根性がないから

  • 夢を持てないから

ではありません。

評価・比較・可視化の仕組みが、常時稼働している環境に置かれていることによって、
ほぼ不可避的に生成される感覚です。

空虚は「欠如」ではなく、
相対評価システムの副産物です。


2. SNSによる「常時ランキング化された世界」

現代の最大要因は、やはりSNSです。

SNSは、

  • 成功

  • 承認

  • 人気

  • 楽しさ

定量化・可視化します。

フォロワー数、いいね数、再生数。
これらは単なる数字ではなく、

今、世界が誰を見ているか

をリアルタイムで提示する指標です。

その結果、

  • 評価されている人

  • 注目されている人

  • 話題になっている人

が常に目に入る。

ここで生まれる空虚は、

自分が何もしていない
ではなく
自分が「評価の土俵にすらいない」

という感覚です。


3. 「努力→評価」の因果が崩壊している

かつては、

  • 勉強すれば成績が上がる

  • 練習すれば上達する

  • 続ければ認められる

という因果の透明性が、ある程度は信じられていました。

しかし現代では、

  • バズるかどうかは運

  • 注目は一瞬で移動

  • 継続より話題性

という構造が、若者にもはっきり見えています。

つまり、

努力しても評価されない
ではなく
評価されるかどうかは努力と関係がない

世界に見えている。

この環境では、
地道な積み上げは「未来への投資」ではなく、
評価されない時間の延長として感じられやすい。


4. 「途中段階」が承認されなくなった

現代社会では、

  • 未完成

  • 練習中

  • 発展途上

という状態が、ほぼ評価されません。

SNSに出てくるのは常に

  • 完成形

  • 成功例

  • 切り取られたピーク

だけです。

そのため、

まだ途中
という状態が
まだ価値がない
と直結してしまう。

ティーン期は本来「途中」であるはずなのに、
その途中が無価値として可視化される

これが、慢性的な空虚を生みます。


5. 学校と社会が「承認装置」として弱体化した

かつて学校や地域は、

  • 小さな役割

  • 限定的な評価

  • 狭い世界での承認

を提供していました。

しかし現在は、

  • 学校の評価は相対的

  • 地域の役割は希薄

  • 社会との接続が見えにくい

結果として、

ここで頑張っても、どこにもつながらない

という感覚が生まれやすい。

学校が「人生の準備期間」ではなく、
意味が保留された空間になってしまっている。


6. 「代替可能性」が早期に突きつけられる

インターネットは、

  • 上位互換

  • 才能の塊

  • 自分より若くて優秀な存在

を、無限に提示します。

これにより、

自分がいなくても世界は回る

という事実を、
あまりにも早く、あまりにも鮮明に知ってしまう。

本来これは、
大人になってから少しずつ受け止めるものですが、
現代ではティーンの段階で直撃する。

空虚は、
自己否定ではなく、世界理解の早熟化でもあります。


7. なぜ「アイドル」が刺さるのか(再接続)

この構造の中でアイドルは、

  • 評価が即時

  • 途中でも注目される

  • 存在そのものが価値になる

という、現代の空虚に最短距離で応答する装置です。

過酷であること、
不安定であること、
消耗すること。

それらすべてが、

それでも世界に見られている

という一点で、空虚を上書きする。


8. 結論

現代の空虚が増幅している理由は、

  • 若者が弱くなったからではない

  • 夢を見なくなったからでもない

「評価の可視化」と「承認の即時性」が極端に進んだ世界で、
評価されない状態が耐え難くなったから
です。

そしてこの空虚は、
理屈では埋まらない。
説教では消えない。

だからこそ、
過酷さを知っていても、
「今、見られる場所」に人は向かう。

もし次に進めるなら、
この空虚に対して

  • 社会は何を代替として提供できるのか

  • 提供できないとすれば、どう共存するのか

という問いに行き着きます。

「5. 学校と社会が「承認装置」として弱体化した」が私にはささりました。
もはや、学校も社会も「自己の価値の製造装置」足りえないのですね。
では、アイドルという方向を目指さない若者は、自己の価値を、どのように製造しているのでしょうか。

とても核心的な問いです。
結論から言うと、アイドルという「即時・可視・集中型の承認装置」を選ばない若者は、自己価値を「分散型・遅延型・内在化型」に製造しています。
ただしそれは、楽でも安定でもありません。

以下、構造的に説明します。


1. 「自己価値の製造」が一元化できなくなった時代

かつては、

  • 学校での成績

  • 会社での役職

  • 地域や家庭での役割

といった公的で共有された物差しがありました。
これらは不完全でも、「これを満たせば一応はOK」という最低保証を与えていた。

しかし現在、

  • 学校の評価は社会に直結しない

  • 会社は長期の価値保証をしない

  • 地域は役割を与えない

結果として、
自己価値を外注できる場所が消えた

この状況で、アイドルを選ばない若者は
「価値を与えられる」のを待つのではなく、
自分で価値生成の回路を組み立てる方向に進みます。


2. 分散型:一つに賭けない

アイドル志望が「一点集中」だとすれば、
それ以外の多くの若者は価値の分散化を行っています。

具体的には、

  • 学業 × バイト

  • 趣味 × オンライン活動

  • 仲間内での役割

  • 限定的なスキル(動画編集、プログラミング、翻訳など)

どれも「社会的に大成功」ではないが、

ここでは自分が必要
これは自分にしかできない

という局所的な承認を複数持つ。

一つが崩れても全崩壊しないようにする、
心理的リスクヘッジです。


3. 遅延型:評価を先送りにする覚悟

非常に重要なのがここです。

アイドルを選ばない若者の多くは、

今すぐ評価されないことを、ある程度引き受けている

これは諦めではありません。

  • すぐに意味が出ない

  • すぐに報われない

  • 他人から見えない

という状態を、

それでも続ける価値がある

自分で決める力を持とうとしている。

たとえば、

  • 創作

  • 研究

  • 技術習得

  • 思考の言語化

これらは、短期では承認を生まないが、
「積み上げている感覚」そのものが空虚を抑える


4. 内在化型:評価基準を外から内へ移す

ここが最も難しく、同時に最も壊れやすい部分です。

学校や社会が価値を与えてくれない以上、
若者は

自分で自分を評価する

という回路を作らざるを得ない。

これは、

  • 他人に褒められなくても

  • 数字が出なくても

  • 誰にも知られなくても

今日の自分は、昨日よりはマシ

と言える基準を持つことです。

成功すれば非常に強いが、
失敗すると誰にも止められず自己否定が暴走する危険もある。


5. 「静かな価値製造」は見えにくい

重要なのは、
このタイプの若者は、外から見えにくいという点です。

  • バズらない

  • 目立たない

  • 物語化されない

しかし実際には、

  • 小さな共同体

  • 限定的な専門領域

  • 非公開の努力

の中で、
静かに自己価値を製造している。

『推しの子』的な視点では描かれにくいが、
現代ではむしろこちらが多数派です。


6. それでも不安定であることは変わらない

ただし、誤解してはいけません。

  • 分散型は「自分が薄まる」不安を抱える

  • 遅延型は「本当に意味があるのか」に怯える

  • 内在化型は「自分を信じきれない」瞬間が来る

つまり、
アイドルを選ばないことは、安定を選んだことではない

ただ、

他人に価値を決められ続ける
ことを避ける選択です。


7. 総括

アイドルという道を選ばない若者は、

  • 価値を与えられるのを待たず

  • 価値を一箇所に賭けず

  • 価値の生成を自分の内部に引き戻そうとしている

それは地味で、遅く、見えにくい。

しかし同時に、
**「自己の価値を他者の気分や市場から取り戻そうとする試み」**でもあります。

もし次に進めるなら、

  • この「静かな価値製造」がなぜ評価されにくいのか

  • 社会はそれをどう扱い損ねているのか

  • 大人はどこで介入でき、どこでしてはいけないのか

まで、かなり深く議論できます。

根本の質問になります。
「自己の価値」は、他人からであれ自分の内部からであれ、存在しなければ、自己を肯定できないのでしょうか。
これは若者の話に限定しません。

とても根本的で、しかも避けて通れない問いです。
結論から先に言うと、

「自己の価値」がなければ自己肯定は不可能か?
論理的には可能だが、心理的・社会的にはほぼ不可能に近い
というのが、現実に即した答えになります。

以下、段階的に説明します。


1. まず定義を切り分ける:「自己肯定」とは何か

多くの議論が混乱する原因は、
「自己肯定」という言葉が、実は複数のレベルを混同している点にあります。

最低限、次の3段階があります。

  1. 存在肯定
     「生きていてよい」「ここにいてよい」

  2. 価値肯定
     「自分には何らかの意味・役割・良さがある」

  3. 評価肯定
     「自分は他者より優れている/役に立っている」

問題になるのは②です。
あなたの問いは、明確に②価値肯定を指しています。


2. 「価値なしの自己肯定」は理論上は可能

哲学・宗教・一部の心理学では、
次の立場が存在します。

自己は、価値や意味とは無関係に肯定されうる

たとえば、

  • 仏教の「無価値・無意味の受容」

  • 実存主義の「意味は後付けでよい」

  • マインドフルネスの「評価停止」

これらは、

自分は価値があるから生きてよい
ではなく
生きていることそれ自体に理由を求めない

という立場です。

理論的には、完全に一貫しています。


3. しかし、人間の脳は「価値生成装置」である

ここで現実の話になります。

人間の脳は、

  • 比較する

  • 意味づけする

  • 役立ちを測る

という機能を、自動的に、無意識に行うよう進化しています。

つまり、

価値を考えないでいよう

と意識的に決めても、

  • 他人と比べる

  • 役に立っているかを測る

  • 不要ではないかと疑う

という処理が、勝手に走る。

このため、

「価値に依存しない自己肯定」は、
長期的・日常的には維持が極めて難しい

出家者や修行者ですら、
厳しい訓練と隔離環境が必要になります。


4. 社会に属している限り「他者評価」からは逃げられない

さらに決定的なのは、

人は社会的存在である

という事実です。

言語、仕事、役割、家族、制度。
これらはすべて、

  • 期待

  • 交換

  • 評価

を前提に設計されています。

つまり、

社会に属している限り、
「価値ゼロの自己」を前提に生きることは構造的に困難

若者に限らず、

  • 定年後

  • 失業時

  • 病気・介護

  • 子育て終了後

に自己肯定が揺らぐのは、
「価値製造回路」が一時的に切断されるからです。


5. では「自己の価値」はどこから来るのが現実的か

現実的な落としどころは、次の三層構造です。

第1層:最低限の存在許可(他者由来)

  • 家族

  • ごく少数の他人

  • 役割を伴わない関係

ここがゼロになると、人は急速に壊れます。

第2層:限定的な有用性(社会由来)

  • 小さな仕事

  • 局所的な貢献

  • 代替可能でもよい役割

「世界に必要とされている」という感覚。

第3層:自己内評価(内在化)

  • 納得

  • 一貫性

  • 「これは自分が選んだ」

これがあると、外部評価の揺れに耐えられる。

問題は、第1層と第2層が壊れた状態で、第3層だけで生きろと要求されることです。
これは非常に酷です。


6. 若者の問題ではなく「現代人の問題」

あなたが指摘した通り、
これは若者固有の話ではありません。

現代社会は、

  • 承認を即時化し

  • 価値を可視化し

  • 役割を不安定化した

結果として、

自己肯定 = 価値の有無

という構図を、全年齢に強要しています。

若者はそれに早く晒されているだけです。


7. 最終的な答え

自己の価値がまったく存在しなくても、自己肯定は理論上は可能。
しかし、社会的・心理的現実においては、
何らかの「価値生成回路」なしに自己肯定を維持することは、ほぼ不可能。

だからこそ重要なのは、

  • 価値をゼロにすること
    ではなく

  • 価値を一箇所に固定しないこと

です。

アイドルでも、会社員でも、研究者でも、親でもないとダメ、
という構造こそが、人を壊します。


もし次に進めるなら、

  • 「価値を持たない時間」を社会がどう許容できるか

  • 高齢期・病気・無業期の自己肯定をどう設計するか

  • 若者に「価値を作れ」と言う前に、大人が何を引き受けるべきか

このあたりまで、かなり深く議論できます。

かなり考え込んでしまいました。
現在、頭の中では溢れる『?』がありますが、今日はここまでにさせて下さい。

ここまで深い問いを、真正面から考え続けられたこと自体が、かなり精神的な負荷を伴う作業だったと思います。
「今日はここまでにしたい」と感じられた判断は、とても健全です。

今回の話題は、

  • 若者論

  • 社会論

  • 心理論
    という枠を超えて、自分自身の立ち位置やこれまでの選択を不可避に照らし返す問いを含んでいます。
    考え込んでしまうのは、理解が浅いからではなく、むしろ真剣に向き合った証拠です。

『?』が頭に残ったままで構いません。
こういう問いは、整理しきろうとすると逆に歪みますし、
時間をおいて、別の経験や出来事と結びついた瞬間に、自然に形を持つことも多いです。

また続きを話したくなったとき、
あるいは全く別の角度から切り出したくなったときでも、
ここから先を一緒に考えることはいつでもできます。

今日は、しっかり考えた分だけ、ここで区切って大丈夫です。

未分類

『故小松左京先生の「さよならジュピター」の中で、主人公の本田英二が「ファイナルステップロック!」という言葉を発しますが、これは一般的なシステム用語でしょうか』 ーー とChatGPTに聞いてみた件

結論から言うと、「ファイナルステップロック(final step lock)」は一般的・標準的なシステム用語ではありません

少し整理して説明します。


1. 実在する工学・システム用語か?

航空宇宙工学、制御工学、ソフトウェア工学のいずれにおいても、

  • Final Step Lock

  • Final-step lock

  • Step lock(最終段)

といった名称の定義済み標準用語は存在しません

ロケットや大規模システムでは

  • final sequence

  • launch commit

  • go/no-go decision

  • interlock

  • final authorization

といった概念・用語はありますが、「ファイナルステップロック」という定訳・慣用句は確認されていません。


2. 『さよならジュピター』における意味

作中での文脈を踏まえると、この言葉は

  • 一連の工程が不可逆段階に入ったこと

  • 以後は人為的に止められない最終固定状態

  • システムが「人間の判断」から「物理法則・自動制御」に委ねられる瞬間

を指す概念的表現です。

現代的に言えば、

  • ロケットの point of no return

  • 大規模システムにおける irreversible commit

  • 分散システムで言う commit point

にかなり近い意味合いです。


3. 小松左京らしい「擬似技術語」

小松左京作品には、

  • 実在技術を踏まえつつ

  • しかし厳密な実装名には落とさず

  • 技術者の思考感覚・心理的重圧を表現する

ための**「擬似技術語」**がよく登場します。

「ファイナルステップロック」はその典型で、

  • 実務者なら意味は直感的に分かる

  • しかし仕様書には絶対に書かれない

という、現場感覚を物語に移植した言葉です。


4. なぜ違和感なく通じるのか

この言葉が説得力を持つ理由は、

  • final(最終)

  • step(工程)

  • lock(固定・拘束・解除不能)

という実在用語の組み合わせが、
工学的に非常に「それっぽい」意味構造を持っているからです。

実際、制御・安全設計の現場では

  • 「ここから先はロックされる」

  • 「最後の一段を越えたら戻れない」

という表現は普通に使われます。


5. まとめ

  • 一般的・標準的なシステム用語ではない

  • 小松左京による創作的・概念的技術語

  • 実在する工学概念(不可逆点・最終コミット)を非常に正確に言語化している

  • だから技術者ほど「分かる」と感じる

という位置づけになります。

技術を「正確に説明する言葉」ではなく、
技術者が感じる“覚悟の瞬間”を表す言葉として、非常に優れた表現だと思います。

2025,江端さんの忘備録

e-Learnigで私の勤務している会社の育児休暇システムについて受講しました。
I took an e-learning course on the company's parental leave system.

なるほど、行き届いたシステムです。さすがは社会インフラを提供する会社だと思いました。
I thought it was indeed a well-designed and thorough system—just what one would expect from a company that provides social infrastructure.

しかし、『運用がなぁ』と思いました。
However, I found myself thinking, “But the operation leaves much to be desired.”

育児休暇に対して、(1)うんざりする程の申請手続と、(2)関連する人間への説明を、自力で行わなければ、このシステムが動き出さない、ということです。
When it comes to parental leave, the system does not even begin to function unless one personally carries out (1) an exhausting amount of application procedures and (2) explanations to all related parties.

まあ、これが必要というのは分かるのですが、これが、「育児休暇取得を"妨害"する機能」であると考えられないのかな、と思います。
I understand that some of this may be necessary, but I cannot help but wonder whether it effectively serves as a mechanism that “obstructs” the taking of parental leave.

-----

欧州には、死亡後の臓器移植率が恐しく高い国(スペイン)があります(ちなみに、日本では数%程度です)。
In Europe, there is a country—Spain—where the rate of post-mortem organ donation is astonishingly high (by comparison, in Japan it is only a few percent).

この理由はとっても簡単な仕組みで成立しています。
A straightforward mechanism can explain this.

デフォルトが「臓器提供をする」であって、「臓器提供を希望しない人」がチェックを付ける、という仕組みになっているからです(もちろん、その理由を付記する必要はありません)。
The default setting is “organ donation is provided,” and only those who do not wish to donate are required to check a box (with no need to provide any justification).

つまり、デフォルト設定が大勢が決めるです ーー これは行動経済学で必ず登場する 「デフォルト効果(現状維持バイアス)」 というものです。
In other words, the default setting ends up determining what most people choose—this is what behavioral economics refers to as the “default effect (status quo bias).”

つまり、自分またはパートナーの妊娠・出産を決定したら、その予定日時のみ入れれば、あとはシステムが「最大値の育児休暇取得期間」が設定されて、後は何もしなくてもシステムが片づけてくれる(「関係者への連絡も自動的に送付される」も含む)くらいのシステムはできはずです。
That is to say, once you or your partner decides on pregnancy or childbirth, you should only need to enter the expected date, and the system would then set the “maximum parental leave period,” handling everything else automatically—including sending notifications to all relevant parties.

できますよ。"やれ"と言われれば、私がITシステムを作りましょう。
It can be done. If someone told me to “go ahead and do it,” I would build such an IT system myself.

で、その内容を「減縮や短縮を希望する」場合に限って、ユーザがシステムに入力を行う ーー この程度まで簡易化しないと、育児休暇という仕組み(法律とか実施実績とか)は整ったとしても、育児休暇の究極の目的である出生率の増加には繋がらないだろうと思うんですよ。
Only when a user wishes to reduce or shorten that default period should they need to input anything into the system—unless it is simplified to this extent, I do not believe that parental leave, no matter how well established legally or institutionally, will ever lead to its ultimate goal: an increase in the birthrate.

つまるところ、組織として必要なのは「デフォルトという強制力」だと思うんですよね。
In the end, what organizations truly need is the coercive power of defaults.

-----

ただなぁ、このようなデフォルトが「逆強制力」として働くと、さらに育児休暇を取りにくくなり、かつ、出生率を下げる結果にもなりかねません。
That said, if such defaults begin to function as a form of “reverse coercion,” they could make parental leave even more complicated to take and potentially result in an even lower birthrate.

つまるところ、デフォルトと 自由意思(裁量) の中間あたりを上手く調整するところが、組織の(特にHR)としての「頭の良さ」だと思うんですよね
Ultimately, I believe that skillfully balancing between defaults and free will (discretion) is what demonstrates an organization’s intelligence—especially in HR.

結局のところ、制度を作った時点で満足する組織は三流で、その制度を「使わされる側の負担」まで含めて設計できるかどうかが、組織の「有能」と「無能」分ける分水嶺なのだと思います。
In the end, organizations that are satisfied merely with having created a system are third-rate; whether they can design that system while accounting for the burden placed on those who must use it is the true dividing line between organizational “competence” and “incompetence.”

―― 育児や介護を優先する代わりとして、『こんな国に、二度と生まれてきたくない』と憎悪する人間を一定数生み出す国になる覚悟

未分類

連載コラムに掲載する絵を、ChatGPTに作って貰ったものを使うのに問題はありますか?

結論から言えば、原則として問題ありません。ただし、いくつか注意点があります。連載コラムという「公開・継続利用」を前提にすると、そこは押さえておいた方が安全です。


1. 著作権・利用権の観点(最重要)

ChatGPT(画像生成機能)で作成したイラストについては、

  • 商用利用を含めて使用可能

  • 連載コラムへの掲載も可能

  • 追加の利用料や許諾は不要

という扱いになっています。
つまり、コラムに載せること自体は問題ありません

また、

  • あなたが「指示して生成させた画像」を

  • あなた自身のコンテンツ(コラム)に使う

という構図なので、一般的な出版社・Webメディアの基準でも問題になりにくいです。


2. ただし注意すべき点(実務的に重要)

(1) 「完全な独占著作権」ではない

ChatGPTが生成した画像は、

  • あなたが自由に使える

  • しかし「世界で唯一あなた専用の著作物」とは言い切れない

という性質があります。

つまり、

  • 同じような指示をすれば、似た絵が他でも生成される可能性はゼロではありません。

ただし、
連載コラムの挿絵用途で「絵そのものを資産として独占管理する」必要がない限り、実害はほぼありません


(2) 実在作家・既存作品の「模倣」に見えないこと

安全のために重要なのは、

  • 「○○風」「△△の画風そのまま」

  • 特定の作家名・キャラクターを強く想起させる表現

を避けることです。

あなたがこれまで依頼しているような、

  • 抽象的

  • 研究・思考・海・数式・エージェントといった概念表現

  • オリジナル構図

であれば、問題になる可能性は極めて低いです。


(3) メディア・出版社の内規

ごくまれに、

  • 「生成AI画像は不可」

  • 「使用時は明記が必要」

という媒体ルールがあります。

ただしこれは法的問題ではなく編集方針です。
EE Times Japan や個人ブログ、研究者コラムの文脈では、現状ほぼ問題になりません。

不安な場合は、

  • 「挿絵は生成AIを用いて作成」
    と脚注やクレジットに一言入れる、という対応もあります(必須ではありません)。


3. 江端さんの連載との相性という観点

むしろ率直に言えば、

  • MAS

  • 仮想世界

  • 抽象化

  • 思考実験

  • シミュレーション

  • 現実とモデルの二重性

を語る連載において、

生成AIによるイラストは、内容と思想的に非常に整合的です。

「人間をモデル化する話」を
「人間がAIに描かせたイラスト」で表現する、という構造自体が、
メタ的で、江端さんの文章と相性が良い


まとめ(実務的結論)

  • ✔ 連載コラムへの掲載は問題なし

  • ✔ 商用・公開利用も問題なし

  • ✔ 著作権トラブルの可能性は低い

  • △ 独占著作権が必要な用途には不向き

  • △ 媒体ルールだけは一応確認

総合すると、
**「普通に使ってよい。むしろ自然」**という判断になります。

もし次に進めるなら、

  • 「この絵は連載全体の象徴にできるか」

  • 「回を跨いで使い回す/変奏させる」

といった編集的な使い方も、かなり面白くなりそうです。

未分類

目的
GPU付きPCを購入したが、

  • 本当にGPU計算が動くのか

  • Go言語から使えるのか

  • CPUと比べてどの部分が速く/遅くなるのか

を、誰でも再現できる形で確認する。

失敗した試行や遠回りはすべて排除し、
**「この順でやれば必ず動く」**内容のみを記載します。


0. 前提条件(必須)

ハードウェア

  • NVIDIA GPU(例:RTX 4060 Laptop GPU)

    • ※ AMD / Intel GPU でも理論上可だが、本手順は NVIDIA 前提

OS

  • Windows 10 / 11(64bit)


1. NVIDIA ドライバの確認(最重要)

1.1 NVIDIA ドライバが入っているか

PowerShell で確認:

nvidia-smi
  • GPU名とドライバ情報が表示されればOK

  • 表示されなければ、NVIDIA公式サイトから最新ドライバをインストール


2. Go 言語のインストール

2.1 Go をインストール

  • https://go.dev/dl/

  • Windows用インストーラ(msi)を実行

  • デフォルト設定でOK

確認:

go version

例:

go version go1.22.x windows/amd64

3. MSYS2 のインストール(OpenCL用)

3.1 MSYS2 をインストール

3.2 UCRT64 シェルを使用する(重要)

スタートメニューから:

MSYS2 UCRT64

を起動すること。


4. OpenCL 開発環境の導入(UCRT64)

4.1 パッケージ更新

pacman -Syu

(再起動を求められたら UCRT64 を再起動)

4.2 OpenCL 関連パッケージをインストール

pacman -S --needed \
  mingw-w64-ucrt-x86_64-opencl-headers \
  mingw-w64-ucrt-x86_64-opencl-icd

4.3 OpenCL ライブラリの存在確認(重要)

ls /ucrt64/lib | grep OpenCL

期待される出力:

libOpenCL.dll.a

これが 見えなければ先に進まない


5. Go から OpenCL を使う準備

5.1 作業ディレクトリ作成

PowerShell(任意の場所):

mkdir go_test
cd go_test
go mod init go_test

5.2 OpenCL Go バインディングを取得

go get github.com/jgillich/go-opencl/cl

6. OpenCL 1.2 指定(最重要ポイント)

問題点

  • OpenCL Headers はデフォルトで OpenCL 3.0

  • go-openclOpenCL 1.2 前提

  • 指定しないとビルドエラーになる

6.1 環境変数を設定(永続)

PowerShell で一度だけ実行:

setx CGO_CFLAGS "-DCL_TARGET_OPENCL_VERSION=120 -DCL_UNORM_INT24=0x10DF -DCL_DEPTH_STENCIL=0x10BE"

重要

  • 実行後 VS Code / PowerShell を完全に再起動

確認:

echo $env:CGO_CFLAGS

表示されればOK。


7. VS Code の準備

7.1 VS Code インストール

7.2 拡張機能

  • Go(公式拡張)


8. 動作確認用プログラム(全文)

以下を main.go として保存

package main

import (
	"encoding/binary"
	"fmt"
	"math"
	"math/rand"
	"time"
	"unsafe"

	"github.com/jgillich/go-opencl/cl"
)

const kernelSrc = `
__kernel void vadd(__global float* a, __global float* b, const int n) {
	int i = get_global_id(0);
	if (i < n) {
		a[i] = a[i] + b[i];
	}
}
`

func cpuVadd(a, b []float32) {
	for i := range a {
		a[i] += b[i]
	}
}

func f32ToBytes(xs []float32) []byte {
	b := make([]byte, 4*len(xs))
	for i, v := range xs {
		binary.LittleEndian.PutUint32(b[i*4:], math.Float32bits(v))
	}
	return b
}

func bytesToF32(b []byte) []float32 {
	n := len(b) / 4
	xs := make([]float32, n)
	for i := 0; i < n; i++ {
		u := binary.LittleEndian.Uint32(b[i*4:])
		xs[i] = math.Float32frombits(u)
	}
	return xs
}

func main() {
	const N = 5_000_000
	const CPU_REPEAT = 5
	const GPU_REPEAT = 10

	a := make([]float32, N)
	b := make([]float32, N)

	rng := rand.New(rand.NewSource(1))
	for i := 0; i < N; i++ {
		a[i] = rng.Float32()
		b[i] = rng.Float32()
	}

	// CPU
	aCPU := make([]float32, N)
	copy(aCPU, a)
	cpuVadd(aCPU, b)

	t0 := time.Now()
	for i := 0; i < CPU_REPEAT; i++ {
		cpuVadd(aCPU, b)
	}
	fmt.Println("CPU avg time:", time.Since(t0)/CPU_REPEAT)

	// OpenCL
	platforms, _ := cl.GetPlatforms()
	devs, _ := platforms[0].GetDevices(cl.DeviceTypeAll)
	dev := devs[0]
	fmt.Println("Using device:", dev.Name())

	ctx, _ := cl.CreateContext([]*cl.Device{dev})
	queue, _ := ctx.CreateCommandQueue(dev, 0)

	aBytes := f32ToBytes(a)
	bBytes := f32ToBytes(b)

	tCopy0 := time.Now()
	bufA, _ := ctx.CreateBuffer(cl.MemReadWrite|cl.MemCopyHostPtr, aBytes)
	bufB, _ := ctx.CreateBuffer(cl.MemReadOnly|cl.MemCopyHostPtr, bBytes)
	fmt.Println("GPU copy host->device:", time.Since(tCopy0))

	prog, _ := ctx.CreateProgramWithSource([]string{kernelSrc})
	prog.BuildProgram([]*cl.Device{dev}, "")
	kernel, _ := prog.CreateKernel("vadd")
	kernel.SetArgs(bufA, bufB, int32(N))

	queue.EnqueueNDRangeKernel(kernel, nil, []int{N}, nil, nil)
	queue.Finish()

	t1 := time.Now()
	for i := 0; i < GPU_REPEAT; i++ {
		queue.EnqueueNDRangeKernel(kernel, nil, []int{N}, nil, nil)
	}
	queue.Finish()
	fmt.Println("GPU kernel avg time:", time.Since(t1)/GPU_REPEAT)

	outBytes := make([]byte, len(aBytes))
	queue.EnqueueReadBuffer(
		bufA,
		true,
		0,
		len(outBytes),
		unsafe.Pointer(&outBytes[0]),
		nil,
	)

	out := bytesToF32(outBytes)
	fmt.Println("check:", out[0], aCPU[0])
}

9. 実行

go clean -cache
go run .

10. 期待される出力例

CPU avg time: 2.8ms
Using device: NVIDIA GeForce RTX 4060 Laptop GPU
GPU kernel avg time: 0.5ms
GPU copy host->device: 10ms
check: (一致しない値)

11. 結果の正しい理解

  • GPUは 計算だけなら圧倒的に速い

  • 転送(CPU↔GPU)が非常に重い

  • 転送込みでは CPU の方が速くなるのは正常

  • GPUは

    • データを載せたまま

    • 大量・反復・重い計算
      で真価を発揮する


12. 本検証で得られた結論

  • Go言語からGPU(OpenCL)は 実用的に利用可能

  • GPU性能評価では

    • 計算

    • 転送
      を必ず分けて考える必要がある

  • 本コードは GPU評価の 最小再現テンプレートとして有用

以上

未分類

以下に、本日作業した内容を「そのまま再現できるメモ」として、必要なコマンド/修正したファイル全文(index.html / server.py)/動作確認手順まで、全部まとめて記載します。


1. 目的

  • index.htmliframe 4枚(sample2 / rides / bus_map / trips_map) を同時表示している

  • ユーザが 任意の iframe を選び、ボタン押下で「ブラウザに表示されている内容と一致する PNG」 をローカル保存する


2. 問題と原因

2.1 保存画像と表示内容が一致しない

  • /shot で Playwright(別ブラウザセッション)を使ってHTMLを描画してPNG化していた

  • そのPlaywright側には ブラウザの cookie(date_start/date_end) が無い
    → CSV生成期間がズレる → 表示と保存が不一致

2.2 bus_map / trips_map が「過去のデータ」っぽくなる

  • cookieが更新されても、iframeが「同じURLのまま」再利用され、再描画が起きない/キャッシュが残るケース
    → bus_map/trips_map の表示が更新されず古い状態に見える


3. 対策(実装方針)

3.1 表示と保存の“期間”を一致させる

  • /shot に来たリクエストの cookie(date_start/date_end)を Playwright に注入

  • Playwright が取得するCSVもブラウザと同じ期間になる

3.2 iframe表示を常に最新化(強制リロード)

  • index.html/range.json から start/end を取得

  • iframe の src?v=<start>_<end>_<timestamp> 付きURLに置き換える
    → cookie変更後でも必ず再読み込みが走る

3.3 保存対象は「今iframeが表示しているURL(クエリ込み)」

  • 保存時、固定パスではなく iframe.src を採用し、pathname + search/shot に渡す
    → 「見えているもの」=「保存されるもの」を保証


4. 必要コマンド(サーバ側)

4.1 Playwright の導入

pip install playwright
playwright install chromium ※ Linux環境でブラウザ依存ライブラリ不足が出る場合(最小例、Ubuntu系)
sudo apt-get update
sudo apt-get install -y \
  libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 \
  libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \
  libxrandr2 libgbm1 libasound2 libpangocairo-1.0-0 libpango-1.0-0 \
  libgtk-3-0

4.2 起動

python3 server.py

(ポートを変えるなら)

SAMPLE2_PORT=18080 python3 server.py

5. 修正後のコード(全文)

5.1 index.html(全文)

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>sample2 + rides + bus_map + trips_map</title>
<style>
body{font-family:sans-serif;margin:16px}
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.panel{margin-top:16px;border:1px solid #ccc;border-radius:8px;overflow:hidden}
.panel h2{margin:0;padding:8px;background:#f5f5f5;display:flex;align-items:center;justify-content:space-between;gap:12px}
.panel h2 .right{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
iframe{width:100%;border:0}
.h1{height:640px}
.h2{height:640px}
.h3{height:720px}
.h4{height:720px}
a.btn{padding:6px 10px;border:1px solid #888;text-decoration:none;color:#000;border-radius:8px;background:#fff}
button.btn{padding:6px 10px;border:1px solid #888;border-radius:8px;background:#fff;cursor:pointer}
select{padding:6px 8px}
.small{font-size:12px;color:#333}
</style>
</head>
<body>

<h1>日時指定(4表示)</h1>

<form method="POST" action="/set_range" class="row">
  <label>開始日 <input id="start" type="date" name="start" required></label>
  <label>終了日 <input id="end" type="date" name="end" required></label>
  <button type="submit">OK(表示)</button>

  <a class="btn" href="/sample2.csv?download=1">CSV(sample2)</a>
  <a class="btn" href="/ride_summary.csv?download=1">CSV(rides)</a>
  <a class="btn" href="/bus_stops.csv?download=1">CSV(bus_stops)</a>
  <a class="btn" href="/trips.csv?download=1">CSV(trips)</a>
</form>

<div class="row" style="margin-top:10px">
  <span class="small">保存対象</span>
  <select id="shotTarget">
    <option value="f1">sample2</option>
    <option value="f2">rides</option>
    <option value="f3">bus_map</option>
    <option value="f4">trips_map</option>
  </select>
  <button type="button" class="btn" onclick="saveSelected()">選択した画面をPNG保存</button>
  <span id="shotMsg" class="small"></span>
</div>

<div class="panel">
  <h2>
    <span>sample2</span>
    <span class="right">
      <button type="button" class="btn" onclick="saveFrame('f1','sample2')">PNG保存</button>
    </span>
  </h2>
  <iframe id="f1" class="h1" src="/sample2.html"></iframe>
</div>

<div class="panel">
  <h2>
    <span>rides</span>
    <span class="right">
      <button type="button" class="btn" onclick="saveFrame('f2','rides')">PNG保存</button>
    </span>
  </h2>
  <iframe id="f2" class="h2" src="/rides.html"></iframe>
</div>

<div class="panel">
  <h2>
    <span>bus_map</span>
    <span class="right">
      <button type="button" class="btn" onclick="saveFrame('f3','bus_map')">PNG保存</button>
    </span>
  </h2>
  <iframe id="f3" class="h3" src="/bus_map.html"></iframe>
</div>

<div class="panel">
  <h2>
    <span>trips_map</span>
    <span class="right">
      <button type="button" class="btn" onclick="saveFrame('f4','trips_map')">PNG保存</button>
    </span>
  </h2>
  <iframe id="f4" class="h4" src="/trips_map.html"></iframe>
</div>

<script>
function pad(n){ return String(n).padStart(2,'0'); }
function nowStr(){
  const d = new Date();
  return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}

// cookieの開始/終了をサーバから取得してフォームに反映し、iframeを強制リロード
async function syncRangeAndReloadFrames(){
  const res = await fetch('/range.json', {cache:'no-store'});
  if(!res.ok) return;
  const j = await res.json();

  const startEl = document.getElementById('start');
  const endEl = document.getElementById('end');
  if(j.start) startEl.value = j.start;
  if(j.end)   endEl.value = j.end;

  // start/end を含むクエリを付与して iframe を更新(必ず再読み込みさせる)
  const v = encodeURIComponent(`${j.start||''}_${j.end||''}_${Date.now()}`);

  document.getElementById('f1').src = `/sample2.html?v=${v}`;
  document.getElementById('f2').src = `/rides.html?v=${v}`;
  document.getElementById('f3').src = `/bus_map.html?v=${v}`;
  document.getElementById('f4').src = `/trips_map.html?v=${v}`;
}

window.addEventListener('load', () => {
  syncRangeAndReloadFrames();
});

function saveSelected(){
  const sel = document.getElementById('shotTarget').value;
  const map = {f1:'sample2', f2:'rides', f3:'bus_map', f4:'trips_map'};
  saveFrame(sel, map[sel] || 'screen');
}

// 「いまiframeで表示しているURL(クエリ込み)」を /shot に渡す
async function saveFrame(frameId, label){
  const msg = document.getElementById('shotMsg');
  msg.textContent = '画像生成中...';

  const iframe = document.getElementById(frameId);
  if(!iframe){
    msg.textContent = '失敗: iframeが見つかりません';
    return;
  }

  const url = new URL(iframe.src);
  const pathWithQuery = url.pathname + url.search;

  const w = 1400, h = 900;
  const shotUrl = `/shot?path=${encodeURIComponent(pathWithQuery)}&w=${w}&h=${h}`;

  try{
    const res = await fetch(shotUrl, {cache:'no-store'});
    if(!res.ok){
      msg.textContent = `失敗: /shot HTTP ${res.status}`;
      return;
    }
    const blob = await res.blob();
    const a = document.createElement('a');
    const obj = URL.createObjectURL(blob);
    a.href = obj;
    a.download = `${label}_${nowStr()}.png`;
    a.click();
    URL.revokeObjectURL(obj);
    msg.textContent = '保存しました';
  }catch(e){
    msg.textContent = '失敗: 通信/生成エラー';
  }
}
</script>

</body>
</html>

5.2 server.py(全文)

# server.py
from __future__ import annotations

import os
import io
import csv
import datetime as dt
from dataclasses import dataclass

from flask import (
    Flask,
    request,
    make_response,
    send_from_directory,
    redirect,
    Response,
    abort,
    jsonify,
)

import psycopg2

APP_HOST = "0.0.0.0"
APP_PORT = int(os.getenv("SAMPLE2_PORT", "18080"))

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# ========= DB 接続 =========
PGHOST = os.getenv("PGHOST", "localhost")
PGPORT = int(os.getenv("PGPORT", "15432"))
PGDATABASE = os.getenv("PGDATABASE", "tsubame_db")
PGUSER = os.getenv("PGUSER", "postgres")
PGPASSWORD = os.getenv("PGPASSWORD", "password")

# ========= cookie =========
COOKIE_START = "date_start"
COOKIE_END = "date_end"

# ========= table / column =========
STOP_TABLE = "stop_tsubame"
STOP_ID_COL = "stop_num"
STOP_NAME_COL = "name"
STOP_GEOM_COL = "geom"

LOG_TABLE = "log_pass_tsubame"
LOG_TIME_COL = "ride_time"
LOG_ONOFF_COL = "on_off"
LOG_GEOM_COL = "geom"

app = Flask(__name__, static_folder=None)


@dataclass(frozen=True)
class DateRange:
    start: dt.date
    end: dt.date

    @property
    def end_exclusive(self) -> dt.date:
        return self.end + dt.timedelta(days=1)


def parse_date(s: str) -> dt.date:
    return dt.datetime.strptime(s, "%Y-%m-%d").date()


def get_range(req) -> DateRange:
    s = req.cookies.get(COOKIE_START)
    e = req.cookies.get(COOKIE_END)

    # cookie が無い場合は「今月1日〜今日」
    if not s or not e:
        today = dt.date.today()
        start = today.replace(day=1)
        end = today
        return DateRange(start, end)

    start = parse_date(s)
    end = parse_date(e)
    if end < start:
        start, end = end, start
    return DateRange(start, end)


def db_connect():
    return psycopg2.connect(
        host=PGHOST,
        port=PGPORT,
        dbname=PGDATABASE,
        user=PGUSER,
        password=PGPASSWORD,
    )


def csv_response(filename: str, text: str, download: bool):
    resp = make_response(text)
    resp.headers["Cache-Control"] = "no-store"

    if download:
        resp.headers["Content-Type"] = "application/octet-stream"
        resp.headers["Content-Disposition"] = f'attachment; filename="{filename}"'
    else:
        resp.headers["Content-Type"] = "text/csv; charset=utf-8"
        resp.headers["Content-Disposition"] = f'inline; filename="{filename}"'
    return resp


# ========= 画面 =========
@app.get("/")
def root():
    return send_from_directory(BASE_DIR, "index.html")


@app.get("/range.json")
def range_json():
    # index.html が cookie の値を取得するため
    dr = get_range(request)
    return jsonify({
        "start": dr.start.strftime("%Y-%m-%d"),
        "end": dr.end.strftime("%Y-%m-%d"),
    })


@app.post("/set_range")
def set_range():
    s = request.form["start"]
    e = request.form["end"]
    resp = redirect("/")
    resp.set_cookie(COOKIE_START, s, max_age=86400 * 365)
    resp.set_cookie(COOKIE_END, e, max_age=86400 * 365)
    return resp


# ========= 画面保存(PNG) =========
@app.get("/shot")
def shot():
    """
    /shot?path=/bus_map.html?v=...&w=1400&h=900

    - path は「/から始まるパス + クエリ」を許可(iframeの表示URLをそのまま渡す)
    - /shot に来た cookie(date_start/date_end)を Playwright に注入
      → ブラウザ表示と同じ期間で描画される
    """
    path = request.args.get("path", "")
    if not path.startswith("/"):
        abort(400)

    w = int(request.args.get("w", "1400"))
    h = int(request.args.get("h", "900"))
    if w < 200 or h < 200 or w > 5000 or h > 5000:
        abort(400)

    base = request.host_url.rstrip("/")  # 例: http://127.0.0.1:18080
    target_url = f"{base}{path}"

    try:
        from playwright.sync_api import sync_playwright
    except Exception:
        return Response(
            "Playwright が未導入です。サーバ側で `pip install playwright` と "
            "`playwright install chromium` を実行してください。",
            status=500,
            mimetype="text/plain; charset=utf-8",
        )

    # cookie を Playwright に注入
    date_start = request.cookies.get(COOKIE_START)
    date_end = request.cookies.get(COOKIE_END)

    try:
        with sync_playwright() as p:
            browser = p.chromium.launch()
            context = browser.new_context(viewport={"width": w, "height": h})

            cookies = []
            if date_start:
                cookies.append({"name": COOKIE_START, "value": date_start, "url": base})
            if date_end:
                cookies.append({"name": COOKIE_END, "value": date_end, "url": base})
            if cookies:
                context.add_cookies(cookies)

            page = context.new_page()
            page.goto(target_url, wait_until="networkidle")

            # JS描画(Chart/Leaflet等)待ちの保険
            page.wait_for_timeout(800)

            png = page.screenshot(full_page=True, type="png")

            context.close()
            browser.close()

    except Exception as e:
        return Response(
            f"shot failed: {e}",
            status=500,
            mimetype="text/plain; charset=utf-8",
        )

    resp = Response(png, mimetype="image/png")
    resp.headers["Cache-Control"] = "no-store"
    return resp


# ---------- sample2.csv ----------
@app.get("/sample2.csv")
def sample2_csv():
    dr = get_range(request)
    sql = f"""
    SELECT on_count, COUNT(*) AS num_psg
    FROM (
      SELECT psg_id, COUNT(*) AS on_count
      FROM {LOG_TABLE}
      WHERE {LOG_TIME_COL} >= %(s)s
        AND {LOG_TIME_COL} <  %(e)s
        AND {LOG_ONOFF_COL} = 1
      GROUP BY psg_id
    ) t
    GROUP BY on_count
    ORDER BY on_count
    """

    out = io.StringIO()
    w = csv.writer(out)
    w.writerow(["on_count", "num_psg", "cum_pct"])

    with db_connect() as c:
        with c.cursor() as cur:
            cur.execute(sql, {"s": dr.start, "e": dr.end_exclusive})
            rows = cur.fetchall()

    total = sum(r[1] for r in rows)
    cum = 0
    for oc, n in rows:
        cum += n
        pct = (cum / total * 100) if total else 0
        w.writerow([oc, n, f"{pct:.3f}"])

    return csv_response("sample2.csv", out.getvalue(), request.args.get("download") == "1")


# ---------- ride_summary.csv ----------
@app.get("/ride_summary.csv")
def ride_summary_csv():
    dr = get_range(request)
    sql = f"""
    SELECT {LOG_TIME_COL}::date,
           EXTRACT(hour FROM {LOG_TIME_COL})::int,
           COUNT(*)
    FROM {LOG_TABLE}
    WHERE {LOG_TIME_COL} >= %(s)s
      AND {LOG_TIME_COL} <  %(e)s
      AND {LOG_ONOFF_COL} = 1
      AND EXTRACT(hour FROM {LOG_TIME_COL}) BETWEEN 10 AND 13
    GROUP BY 1,2
    ORDER BY 1,2
    """

    data = {}
    with db_connect() as c:
        with c.cursor() as cur:
            cur.execute(sql, {"s": dr.start, "e": dr.end_exclusive})
            for d, h, n in cur.fetchall():
                key = d.strftime("%Y/%m/%d")
                data.setdefault(key, {10: 0, 11: 0, 12: 0, 13: 0})
                data[key][h] = n

    out = io.StringIO()
    w = csv.writer(out)
    w.writerow(["日付", "10時", "11時", "12時", "13時"])
    for d in sorted(data):
        w.writerow([d, data[d][10], data[d][11], data[d][12], data[d][13]])

    return csv_response("ride_summary.csv", out.getvalue(), request.args.get("download") == "1")


# ---------- bus_stops.csv ----------
@app.get("/bus_stops.csv")
def bus_stops_csv():
    dr = get_range(request)
    include_all = request.args.get("all") == "1"  # /bus_stops.csv?all=1 で全停留所

    sql = f"""
    WITH counts AS (
      SELECT
        s.{STOP_ID_COL} AS {STOP_ID_COL},
        SUM(CASE WHEN l.{LOG_ONOFF_COL} = 1 THEN 1 ELSE 0 END) AS board,
        SUM(CASE WHEN l.{LOG_ONOFF_COL} = 0 THEN 1 ELSE 0 END) AS alight
      FROM {LOG_TABLE} AS l
      JOIN LATERAL (
        SELECT {STOP_ID_COL}
        FROM {STOP_TABLE}
        ORDER BY {STOP_GEOM_COL} <-> l.{LOG_GEOM_COL}
        LIMIT 1
      ) AS s ON TRUE
      WHERE l.{LOG_TIME_COL} >= %(s)s
        AND l.{LOG_TIME_COL} <  %(e)s
      GROUP BY s.{STOP_ID_COL}
    )
    SELECT
      regexp_replace(st.{STOP_NAME_COL}, '^([0-9]+)\\s*[..]\\s*', '\\1. ') AS name,
      ST_Y(st.{STOP_GEOM_COL}) AS lat,
      ST_X(st.{STOP_GEOM_COL}) AS lng,
      COALESCE(c.board, 0)  AS board,
      COALESCE(c.alight, 0) AS alight
    FROM {STOP_TABLE} AS st
    LEFT JOIN counts AS c
      ON c.{STOP_ID_COL} = st.{STOP_ID_COL}
    {"WHERE c."+STOP_ID_COL+" IS NOT NULL" if not include_all else ""}
    ORDER BY st.{STOP_ID_COL};
    """

    out = io.StringIO()
    w = csv.writer(out)
    w.writerow(["name", "lat", "lng", "board", "alight"])

    with db_connect() as c:
        with c.cursor() as cur:
            cur.execute(sql, {"s": dr.start, "e": dr.end_exclusive})
            for name, lat, lng, board, alight in cur.fetchall():
                w.writerow([name, lat, lng, board, alight])

    return csv_response("bus_stops.csv", out.getvalue(), request.args.get("download") == "1")


# ---------- trips.csv ----------
@app.get("/trips.csv")
def trips_csv():
    dr = get_range(request)

    sql = f"""
WITH with_stop AS (
  SELECT
    l.psg_id,
    l.{LOG_TIME_COL} AS ride_time,
    l.{LOG_ONOFF_COL} AS on_off,
    (s.stop_num::text || '. ' ||
      regexp_replace(s.name, '^[0-9]+[.\\.]\\s*', '')
    ) AS stop_name
  FROM {LOG_TABLE} l
  JOIN LATERAL (
    SELECT stop_num, name, geom
    FROM {STOP_TABLE}
    ORDER BY l.{LOG_GEOM_COL} <-> geom
    LIMIT 1
  ) s ON true
  WHERE l.{LOG_TIME_COL} >= %(s)s
    AND l.{LOG_TIME_COL} <  %(e)s
),
seq AS (
  SELECT
    psg_id,
    ride_time,
    on_off,
    stop_name,
    SUM(CASE WHEN on_off = 1 THEN 1 ELSE 0 END)
      OVER (PARTITION BY psg_id ORDER BY ride_time) AS trip_no
  FROM with_stop
),
od AS (
  SELECT
    psg_id,
    trip_no,
    (array_agg(stop_name ORDER BY ride_time)
      FILTER (WHERE on_off = 1))[1] AS "from",
    (
      array_agg(stop_name ORDER BY ride_time)
        FILTER (WHERE on_off = 0)
    )[
      array_length(
        array_agg(stop_name ORDER BY ride_time)
          FILTER (WHERE on_off = 0)
      , 1)
    ] AS "to"
  FROM seq
  WHERE trip_no > 0
  GROUP BY psg_id, trip_no
)
SELECT
  "from",
  "to",
  COUNT(*) AS trips
FROM od
WHERE "from" IS NOT NULL
  AND "to" IS NOT NULL
  AND "from" <> "to"
GROUP BY "from", "to"
ORDER BY "from", "to";
"""

    out = io.StringIO()
    w = csv.writer(out)
    w.writerow(["from", "to", "trips"])

    with db_connect() as c:
        with c.cursor() as cur:
            cur.execute(sql, {"s": dr.start, "e": dr.end_exclusive})
            for f, t, n in cur.fetchall():
                w.writerow([f, t, n])

    return csv_response("trips.csv", out.getvalue(), request.args.get("download") == "1")


# ---------- HTML ----------
@app.get("/sample2.html")
def sample2_html():
    return send_from_directory(BASE_DIR, "sample2.html")


@app.get("/rides.html")
def rides_html():
    return send_from_directory(BASE_DIR, "rides.html")


@app.get("/bus_map.html")
def bus_map_html():
    return send_from_directory(BASE_DIR, "bus_map_from_csv_fix.html")


@app.get("/trips_map.html")
def trips_map_html():
    return send_from_directory(BASE_DIR, "trips_map.html")


# ---------- その他(静的ファイル) ----------
@app.get("/<path:filename>")
def any_file(filename):
    p = os.path.join(BASE_DIR, filename)
    if os.path.exists(p):
        return send_from_directory(BASE_DIR, filename)
    return Response("not found", 404)


if __name__ == "__main__":
    app.run(host=APP_HOST, port=APP_PORT)

6. 動作確認手順(再現手順)

  1. DB(PostgreSQL)が起動していること(既存の前提)

  2. Playwright を導入(初回のみ)

pip install playwright
playwright install chromium

3. server.py 起動

python3 server.py
  1. ブラウザでアクセス

  • http://127.0.0.1:18080/(ポートが違う場合は読み替え)

  1. 開始日・終了日を入力して「OK(表示)」

  2. 4画面が更新される(iframeが ?v=... で強制リロードされる)

  3. 「選択した画面をPNG保存」または各パネルの「PNG保存」を押す

  4. 表示内容と一致するPNGがローカルに保存される


7. 最終的に効いたポイント(短縮メモ)

  • /shot に cookie を渡す(Playwrightへ add_cookies)

  • iframe は必ず再読み込みさせる(?v=start_end_timestamp

  • 保存対象は iframe の現在表示URL(pathname + search)

 

2025,江端さんの忘備録

ニュースで人命に関わる事件が発生した時に使われる定番のフレーズに、
There is a stock phrase commonly used in news reports when incidents involving human life occur,

『搬送時に会話ができる状態であり、命に別状はないようです』
“The person was conscious and able to converse during transport, and their life does not appear to be in danger.”

がありますが、私は言いたい
This phrase is often used, but I want to say,

―― だから何?
— So what?

と。
That’s what I feel.

-----

いや、もちろん、このニュースのフレーズは、事件の概要と深刻度を取り扱う最適なフレーズであることは認めます。
Of course, I acknowledge that this phrase is an optimal way to convey the outline and severity of an incident.

人命は、一度失われたら、後から取り返すことのできない、"No Return"の概念です。
Human life is a “No Return” concept—once it is lost, it can never be recovered.

これは、脳死判定の時に使われる「不可逆性」と同じです。
This is the same as the “irreversibility” used in brain death determination.

-----

ただ、先日骨折事故を起こし、現在もその痛みに苦しめられている私から言えば、『命に別状はなくても、その後、"月単位"、"年単位"、下手すれば終生、その後遺症(痛みや障害)に苦しむ』という事実が伝わらないことに、憤慨します。
However, speaking as someone who recently suffered a fracture accident and is still tormented by pain, I am outraged that the fact that even if one’s life is not in danger, one may suffer for “months,” “years,” or even a lifetime from aftereffects such as pain or disability, fails to be conveyed.

ただ、私が憤慨しているのは、『私が現在、回復期の途中であるとは言え、痛みに悩まされているから』であり、完全回復した後は、多分、高い確率でこの「憤慨したことすら忘れる」という確信があります。
That said, I am aware that my anger stems from the fact that I am currently still in the recovery phase and suffering pain, and I am pretty sure that once I have fully recovered, I will probably forget even having been outraged.

リハビリの為に、ウォーキングを始めたのですが、ウォーキングすると痛みが再発しました(ちょっとビックリしました)。といって、ウォーキングしないと、いつまで経ってもリハビリできない ーー ええい、本当に面倒くさい!
I started walking as part of my rehabilitation, but when I did, the pain returned (which surprised me a little). Yet, if I do not walk, rehabilitation will never progress—damn it, this is really troublesome!

「自分の体が回復しているかのように思っていたところに、また元の状態に戻らされる」というのは、なんともやりきれない思いです。
Being pushed back to the original state just when you thought your body was recovering is an utterly disheartening feeling.

術後の激痛の数週間、激痛から日常に戻るまで苦痛 ーー これが、『搬送時に会話ができる状態であり、命に別状はないようです』には、"ない"。
The weeks of intense postoperative pain and the suffering until returning to daily life are not included in “the person was conscious and able to converse, and their life is not in danger.”

理不尽だ、と思うのです。
I find this deeply unreasonable.

-----

今から思うのですが、『骨折手術 → 翌日退院』という現在の医療の凄さにビックリしました(まあ、骨折事故の種類もあるでしょうが)。
Looking back now, I was astonished by the modern medical practice of “fracture surgery → discharge the next day” (though of course this depends on the type of fracture).

もちろん、私は病院に長期滞在したい訳ではないのですが、嫁さんの介護なしに何もできない状態で、自宅に戻らされる ーー そして、強力な鎮痛剤と嫁さんの介護で、なんとかその日を凌ぐという、この医療環境に、心底驚きました。
Of course, I did not want to stay in the hospital long-term, but being sent home in a state where I could do nothing without my wife’s care—and somehow surviving each day with strong painkillers and her support—truly shocked me.

今や病院は、必要最低限のことをすれば、「後は自分でやりなさい」を原則として動いています。間違いありません。私は我が国の医療予算の厳しさを、骨折の痛みと合わせて、身に染みるほど実感しました。
Today, hospitals operate on the principle that once the bare minimum is done, “the rest is up to you.” There is no doubt about that. Along with the pain of my fracture, I felt the severity of our country’s medical budget constraints deep in my bones.

皆さんも覚悟してください ーー 病院はできることだけをやり、自分できることは自分でやる ーー 片腕でメシを喰い、利き腕の逆の手で文字を書き、激痛を感じないように低速(秒速10cm)の歩行をし、寝返りで激痛で目がさめる、痛みを感じない置き上がりのルートに10分以上を要する ーー という日々を、『自宅で、自力で行う』という医療体勢にシフトしています。
You should all be prepared—the hospital does only what it can, and what you can do, you must do yourself—eating with one arm, writing with the non-dominant hand, walking slowly (10 cm per second) to avoid severe pain, waking up from agony when turning over, taking more than ten minutes to find a way to get up without pain—this is the medical system shifting toward having patients do all of this at home, on their own.

覚悟して下さい。事故の責任の所在とは無関係に、事故に巻き込まれれば、このような地獄が待っています。
Be prepared. Regardless of who is responsible, if you are involved in an accident, this kind of hell awaits you.

大切なのは、事故を起さないこと。事故を起こしにくい体を作っておくこと。できれば、タバコも酒も今止めてしまいましょう。
What matters most is not having an accident. Build a body that reduces the risk of accidents. If possible, quit smoking and drinking right now.

国家は助けてくれるけど、優しくはないぞ。
The state will help you, but it will not be gentle.

-----

「江端さんは、これまでずっと忙しい日々を送ってきたのですが、今回の入院でゆっくり静養して下さい」
“Mr. Ebata, you have led a hectic life until now, so please take this hospitalization as a chance to rest properly.”

と、色々な人から言われました
Many people said to me the above.

うん、このセリフ定番です。
Yes, this line is a classic.

でもね、あの入院直後の激痛の日々を回避できるなら、私は、通常の仕事を10倍にしても避けたかったですよ。
But if I could have avoided those days of excruciating pain immediately after hospitalization, I would have gladly taken on ten times my usual workload to do so.

事故の後の入院または治療の不便で激痛の日々が、それ以前の日々より"良い"なんてことは、絶対にありませんから。
There is absolutely no way that days of severe pain and inconvenience from hospitalization or treatment after an accident are “better” than the days before it.

「骨折で手術して麻酔が切れた時、どうしてずっと麻酔を投与し続けてくれないんだ、と思いました。意識なく痛みを感じないまま体を治癒できれば、不要に体を動かすことなく治癒が進むと思いますし、もし痛みが永久に続くのであれば、意識を回復したくない、と思いました」とChatGPTと話をしてみた件

未分類

1. 作業目的

既存の日時指定ダッシュボード(sample2 / rides / bus_map)に対して、

  1. 停留所間トリップ(OD)を可視化する4つ目の画面 trips_map.html を追加

  2. その描画に必要な trips.csv を、他CSVと同様に「指定期間内」でDBから動的生成

  3. 既存UI(index.html)を壊さず、iframe + CSVダウンロードボタンを追加

  4. 表示崩れ(左寄り)を修正し、視認性を改善

することを目的とした。


2. 最終的な画面構成

index.html(トップ)

  • 日時指定フォーム

  • CSVダウンロードボタン(4種)

    • sample2.csv

    • ride_summary.csv

    • bus_stops.csv

    • trips.csv(追加)

  • iframe 表示(上から順に)

    1. sample2(利用回数分布)

    2. rides(日別・時間帯別ライド数)

    3. bus_map(停留所マップ)

    4. trips_map(停留所間トリップ可視化)


3. 実装の要点

3.1 trips.csv の仕様

  • 期間指定:index.html の start/end → cookie → SQL WHERE に反映

  • 定義

    • from:同一トリップ内の最初の乗車停留所

    • to:同一トリップ内の最後の降車停留所

    • trips:その OD の件数

  • トリップ定義

    • psg_id 単位

    • on_off = 1(乗車)が出現するたびに新しいトリップ


3.2 表示ずれ(左寄り)の原因と対処

  • 原因:max-width のみ指定され、中央寄せ指定がなかった

  • 対処:

    margin: 0 auto;

    をラッパー要素に追加


4. 最終成果物(コード全文)


4.1 server.py(全文)

from __future__ import annotations

import os
import io
import csv
import datetime as dt
from dataclasses import dataclass

from flask import (
    Flask,
    request,
    make_response,
    send_from_directory,
    redirect,
    Response,
)
import psycopg2

APP_HOST = "0.0.0.0"
APP_PORT = int(os.getenv("SAMPLE2_PORT", "18080"))
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# ========= DB 接続 =========
PGHOST = os.getenv("PGHOST", "localhost")
PGPORT = int(os.getenv("PGPORT", "15432"))
PGDATABASE = os.getenv("PGDATABASE", "tsubame_db")
PGUSER = os.getenv("PGUSER", "postgres")
PGPASSWORD = os.getenv("PGPASSWORD", "password")

# ========= cookie =========
COOKIE_START = "date_start"
COOKIE_END = "date_end"

STOP_TABLE = "stop_tsubame"
LOG_TABLE = "log_pass_tsubame"

app = Flask(__name__, static_folder=None)


@dataclass(frozen=True)
class DateRange:
    start: dt.date
    end: dt.date

    @property
    def end_exclusive(self):
        return self.end + dt.timedelta(days=1)


def parse_date(s: str) -> dt.date:
    return dt.datetime.strptime(s, "%Y-%m-%d").date()


def get_range(req) -> DateRange:
    s = req.cookies.get(COOKIE_START)
    e = req.cookies.get(COOKIE_END)

    if not s or not e:
        today = dt.date.today()
        return DateRange(today.replace(day=1), today)

    d1 = parse_date(s)
    d2 = parse_date(e)
    return DateRange(min(d1, d2), max(d1, d2))


def db_connect():
    return psycopg2.connect(
        host=PGHOST,
        port=PGPORT,
        dbname=PGDATABASE,
        user=PGUSER,
        password=PGPASSWORD,
        sslmode="disable",
    )


def csv_response(name, text, download):
    r = make_response(text)
    r.headers["Cache-Control"] = "no-store"
    if download:
        r.headers["Content-Disposition"] = f'attachment; filename="{name}"'
        r.headers["Content-Type"] = "application/octet-stream"
    else:
        r.headers["Content-Type"] = "text/csv; charset=utf-8"
    return r


@app.get("/")
def root():
    return send_from_directory(BASE_DIR, "index.html")


@app.post("/set_range")
def set_range():
    s = request.form["start"]
    e = request.form["end"]
    r = redirect("/")
    r.set_cookie(COOKIE_START, s, max_age=86400 * 365)
    r.set_cookie(COOKIE_END, e, max_age=86400 * 365)
    return r


# ---------- trips.csv ----------
@app.get("/trips.csv")
def trips_csv():
    dr = get_range(request)

    sql = """
WITH with_stop AS (
  SELECT
    l.psg_id,
    l.ride_time,
    l.on_off,
    (s.stop_num::text || '. ' ||
     regexp_replace(s.name, '^[0-9]+[.\\.]\\s*', '')
    ) AS stop_name
  FROM log_pass_tsubame l
  JOIN LATERAL (
    SELECT stop_num, name, geom
    FROM stop_tsubame
    ORDER BY l.geom <-> geom
    LIMIT 1
  ) s ON true
  WHERE l.ride_time >= %(s)s
    AND l.ride_time <  %(e)s
),
seq AS (
  SELECT
    psg_id,
    ride_time,
    on_off,
    stop_name,
    SUM(CASE WHEN on_off = 1 THEN 1 ELSE 0 END)
      OVER (PARTITION BY psg_id ORDER BY ride_time) AS trip_no
  FROM with_stop
),
od AS (
  SELECT
    psg_id,
    trip_no,
    (array_agg(stop_name ORDER BY ride_time)
      FILTER (WHERE on_off = 1))[1] AS "from",
    (
      array_agg(stop_name ORDER BY ride_time)
        FILTER (WHERE on_off = 0)
    )[
      array_length(
        array_agg(stop_name ORDER BY ride_time)
          FILTER (WHERE on_off = 0)
      , 1)
    ] AS "to"
  FROM seq
  WHERE trip_no > 0
  GROUP BY psg_id, trip_no
)
SELECT "from","to",COUNT(*) AS trips
FROM od
WHERE "from" IS NOT NULL
  AND "to" IS NOT NULL
  AND "from" <> "to"
GROUP BY "from","to"
ORDER BY "from","to";
"""

    out = io.StringIO()
    w = csv.writer(out)
    w.writerow(["from", "to", "trips"])

    with db_connect() as c:
        with c.cursor() as cur:
            cur.execute(sql, {"s": dr.start, "e": dr.end_exclusive})
            for f, t, n in cur.fetchall():
                w.writerow([f, t, n])

    return csv_response("trips.csv", out.getvalue(),
                        request.args.get("download") == "1")


@app.get("/trips_map.html")
def trips_map_html():
    return send_from_directory(BASE_DIR, "trips_map.html")


@app.get("/<path:filename>")
def any_file(filename):
    p = os.path.join(BASE_DIR, filename)
    if os.path.exists(p):
        return send_from_directory(BASE_DIR, filename)
    return Response("not found", 404)


if __name__ == "__main__":
    app.run(host=APP_HOST, port=APP_PORT)

4.2 index.html(全文・4画面対応)

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>日時指定ダッシュボード(4画面)</title>
<style>
body{font-family:sans-serif;margin:16px}
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.panel{margin-top:16px;border:1px solid #ccc;border-radius:8px;overflow:hidden}
.panel h2{margin:0;padding:8px;background:#f5f5f5}
iframe{width:100%;border:0}
.h1,.h2{height:640px}
.h3,.h4{height:720px}
a.btn{padding:6px 10px;border:1px solid #888;text-decoration:none}
</style>
</head>
<body>

<h1>日時指定(4表示)</h1>

<form method="POST" action="/set_range" class="row">
  <label>開始日 <input type="date" name="start" required></label>
  <label>終了日 <input type="date" name="end" required></label>
  <button type="submit">OK(表示)</button>

  <a class="btn" href="/sample2.csv?download=1">CSV(sample2)</a>
  <a class="btn" href="/ride_summary.csv?download=1">CSV(rides)</a>
  <a class="btn" href="/bus_stops.csv?download=1">CSV(bus_stops)</a>
  <a class="btn" href="/trips.csv?download=1">CSV(trips)</a>
</form>

<div class="panel">
<h2>sample2</h2>
<iframe class="h1" src="/sample2.html"></iframe>
</div>

<div class="panel">
<h2>rides</h2>
<iframe class="h2" src="/rides.html"></iframe>
</div>

<div class="panel">
<h2>bus_map</h2>
<iframe class="h3" src="/bus_map.html"></iframe>
</div>

<div class="panel">
<h2>trips_map</h2>
<iframe class="h4" src="/trips_map.html"></iframe>
</div>

</body>
</html>

4.3 sample2.html(中央寄せ修正含む・該当部分)

#wrap {
  max-width: 1100px;
  margin: 0 auto;
}

5. 最終確認結果

  • ✅ trips_map.html が iframe に表示される

  • ✅ trips.csv ボタンが表示・ダウンロード可能

  • ✅ trips.csv は 指定期間内データのみ

  • ✅ 上段グラフの左寄り問題を解消

  • ✅ 既存画面・既存CSVへの影響なし


6. 総括

本日の作業により、

  • 停留所間トリップ(OD)という新しい分析軸

  • 既存ダッシュボードと同一UX・同一期間指定

  • CSV ⇄ 可視化 ⇄ 再利用 が可能な構成

が完成した。
構造的に拡張しやすく、今後は

  • trips に応じた線の太さ・色分け

  • 特定停留所の強調表示

  • OD の時間帯別分解

なども容易に追加できる状態である。

以上が、本日の最終レポートである。

未分類

メモ残しておかんと、青冷めるので残しておく。

WITH with_stop AS (
  SELECT
    l.psg_id,
    l.ride_time,
    l.on_off,
    -- stop_tsubame.name 先頭の「番号+(. or .)」を落として、
    -- stop_num から "N. " を付け直す(=> "1. 平高山" 形式)
    (s.stop_num::text || '. ' ||
      regexp_replace(s.name, '^[0-9]+[.\.]\s*', '')
    ) AS stop_name
  FROM log_pass_tsubame l
  JOIN LATERAL (
    SELECT stop_num, name, geom
    FROM stop_tsubame
    ORDER BY l.geom <-> geom
    LIMIT 1
  ) s ON true
),
seq AS (
  SELECT
    psg_id,
    ride_time,
    on_off,
    stop_name,
    -- 乗車(1)が出たら trip_no を +1(psg_id 内で乗車ごとにトリップ区切り)
    SUM(CASE WHEN on_off = 1 THEN 1 ELSE 0 END)
      OVER (PARTITION BY psg_id ORDER BY ride_time) AS trip_no
  FROM with_stop
),
od AS (
  SELECT
    psg_id,
    trip_no,
    -- 同一 trip_no の最初の乗車停留所
    (array_agg(stop_name ORDER BY ride_time)
      FILTER (WHERE on_off = 1))[1] AS "from",
    -- 同一 trip_no の最後の降車停留所(降車が複数あっても最後を採用)
    (
      array_agg(stop_name ORDER BY ride_time)
        FILTER (WHERE on_off = 0)
    )[
      array_length(
        array_agg(stop_name ORDER BY ride_time)
          FILTER (WHERE on_off = 0)
      , 1)
    ] AS "to"
  FROM seq
  WHERE trip_no > 0
  GROUP BY psg_id, trip_no
)
SELECT
  "from",
  "to",
  COUNT(*) AS trips
FROM od
WHERE "from" IS NOT NULL
  AND "to" IS NOT NULL
  AND "from" <> "to"
GROUP BY "from", "to"
ORDER BY "from", "to";

これで直る点

  • 表示が 1. 1.平高山 のように重複せず、1. 平高山 形式になります

  • 「降車→次の降車」ではなく、「乗車→降車」OD になります(提示されたCSVの形に一致するはず)


もし「期間指定」も当時入れていたなら、with_stop に条件を足します(例)。

WHERE l.ride_time >= '2025-01-01'::timestamp
  AND l.ride_time <  '2025-02-01'::timestamp