AIが見落とす「データの罠」――賃貸市場分析で学んだ前処理の本当の難しさ

「データさえあればAIが分析してくれる」――そう思っていないだろうか。

筆者は最近、ある地域の賃貸物件データを使って家賃トレンドの分析を行い、その結果を別の記事として公開した。「物価高騰は賃貸家賃に波及していない」「1月は割高、2月以降は割安」「追焚設備が家賃の最強予測変数」といった知見が得られた分析だ。

だがその裏側では、本格的な分析を始める前に、何時間もかけてデータセットの「確立」に苦労していた。AIを使いながらも、AIだけでは絶対に見つけられない問題が次々と現れた。そしてその問題を見逃していたら、統計的には「有意」に見えるが事実とは異なる間違った結論が導き出されていた。

今回はその前処理の過程を正直に公開する。データサイエンスの「格好いい部分」の前にある、泥臭くて地味だが最も重要な工程の話だ。


そもそも「前処理」とは何か

データ分析の工程は大まかに以下の流れになる。

  1. データ収集(スクレイピング等)
  2. 前処理・データセット確立 ← ここが今回のテーマ
  3. 探索的分析(EDA)
  4. モデル構築・検定
  5. 結果の解釈・レポート

前処理とは「生データを分析できる状態に整える」作業のことだ。具体的には欠損値の処理、型変換、外れ値の除去、重複の排除などが含まれる。

教科書的にはシンプルに聞こえる。しかし現実のデータは教科書とは全く違う顔を持っている。


罠①「文字列で格納された数値」――AIは気づかない

スクレイピングで取得した家賃データの形式はこうだった。

rent: "4.1万円"
admin: "4500円"
sikik: "1.5万円"

数値のように見えるが、Pythonの内部では全て文字列(string)型として格納されている。そのままモデルに渡せばエラーになるか、全件NaN(欠損値)になる。

「4.1万円」→ 41,000円への変換は一見単純だが、実際のデータには「-」(敷金なし)「応相談」「無料」など例外が無数に存在した。変換ロジックを書いても、次々と変換失敗するパターンが出てくる。

AIへの指示で自動変換を試みたところ、AIは「変換成功」と報告した。しかし実際には変換失敗した行が大量にNaN化していた。AIは処理を実行したことは正確に報告するが、結果の妥当性を自ら検証する習慣を持たない。確認コードを別途書いて人間が検証する必要があった。

データ解析と問題発見
データ解析と問題発見

罠②「DateTime型がモデルに混入する」――エラーなく通ってしまう

データには取得日(today)と取得タイムスタンプ(source_timestamp)という時系列の列があった。これらは当然、説明変数から除外すべき列だ。

ところがStandardScaler(標準化処理)にこれらをそのまま渡したとき、エラーは出なかった。Pythonは自動的にdatetime型を数値に変換して処理を続行したのだ。

結果として「取得日が新しいほど家賃が高い/低い」という意味のない相関がモデルに混入し、係数が歪んでいた。エラーが出ないため発見が遅れた。AIもこの異常を指摘しなかった。

「エラーなく動く」≠「正しく動いている」。これが前処理の恐ろしさだ。


罠③「ユニークキーの誤設定」――最も危険な罠

今回のデータは「同一物件を複数の取得日にわたってスクレイピングした」時系列パネルデータだ。分析するには「1物件 = 1レコード」に集約する必要がある。そのためにはまず「同一物件を識別するユニークキー」を正しく定義しなければならない。

これが最も深刻な罠だった。3回の定義変更を経て、ようやく正しい答えにたどり着いた。

v1: 物件名(title)単体をキーとして使う

最初は「物件名が同じなら同一物件」と考えた。しかしすぐに問題が発覚した。

「Tiare」「Bonheur」のようなマンション名は、同一棟の複数の部屋(1階・2階・異なる間取り)が同じ名前で掲載されている。title単体でキーを引くと、本来は別レコードであるべき複数の部屋が1件に誤集約される。

v2: SUUMO物件コードをキーとして使う

次に「サイトが付与している物件コード(suumo_code)が最も信頼できるはず」と考えた。しかしここに大きな落とし穴があった。

大手賃貸サイトは同一物件の掲載コードを定期的に更新する仕様だった。

これを確認したときのデータはこうだった。

suumo_code観測期間取得率
1004930303312026-03-10〜03-101日のみ
1004930422852026-03-13〜03-202日

これは実際には同一物件なのに、コード更新によって2件に分裂している。レオパレス系の物件では取得率の中央値がわずか26.5%――つまり38時点中約10回しか同じコードで観測されていない。同一物件が平均4コードに分裂していた計算になる。

この誤集約版でモデルを動かしたときのR²(決定係数)は0.836。見かけ上は非常に高精度なモデルに見えた。しかし実態は水増しされたサンプル数による見せかけの精度だった。

v3(最終): Union-Findアルゴリズムで連結する

最終的な解決策は、Union-Find(素集合データ構造)というアルゴリズムを使った連結だ。

「物件名・不動産会社コード・階数・間取り・家賃が同一で、旧コードの最終観測日と新コードの初回観測日のギャップが7日以内」という条件を満たすものを同一物件として連結する。

連結前: 1,522件
連結後: 1,371件(151件を連結)
募集期間の中央値: 4日(異常)→ 52日(現実的)

募集期間の中央値が4日から52日に変化した時点で「ようやく正しい集約ができた」と確認できた。入居が決まるまでの期間として4日は明らかに異常で、52日は現実的な値だ。

そして正しい集約後のモデルのR²は0.750。v2の0.836より低い。これが正しい精度だ。高いR²が必ずしも「良いモデル」を意味しない典型例だ。


罠④「多重共線性」――変数を増やすほど精度が下がる逆転現象

ユニークキーが確立した後、次の罠が待っていた。

間取り(1K・2LDKなど)と専有面積(㎡)は、直感的には別の情報のように見える。しかし統計的にはほぼ同じ情報を異なる形で表しているに過ぎない。

両方を同時にモデルに投入すると、VIF(分散拡大係数)が以下のようになった。

変数VIF判定
madori_1K(間取り)69.8❌ 深刻
madori_2LDK57.2❌ 深刻
menseki(面積)22.5❌ 問題

VIF>10は多重共線性ありの目安とされる。間取りダミー変数を全て除外して面積のみにしたところ、VIFは全変数で10以下に収まり、調整済みR²も改善した。

「変数を増やす = 精度が上がる」という思い込みは危険だ。不適切な変数の追加は、むしろモデルを壊す。


「見かけの下落トレンド」が前処理の重要性を証明した

これら全ての前処理を経て初めて、時系列分析が意味を持つようになった。

生データの平均家賃をそのまま時系列でプロットすると、月▲380円という明確な下落トレンドが見えた(p<0.001)。もし前処理が不十分なままここで分析を終えていれば、「この地域の家賃は有意に下落している」と結論づけていただろう。

しかし物件条件(面積・築年数・設備など)を除去したヘドニック残差で同じ分析をすると、トレンドは月▲106円でp値=0.190——統計的に有意でない

「下落トレンド」の正体は、安価な物件の大量新規掲載によるミックス効果だった。前処理が正しくなければ、この区別は絶対にできなかった。


なぜAIだけでは限界があるのか

今回の分析でAIは非常に重要な役割を果たした。Pythonコードの生成、統計的な解釈、可視化スクリプトの作成——これらはAIなしでは数倍の時間がかかっていた。

しかし以下の判断は、人間の「データへの疑い」なしには気づけなかった。

問題なぜAIが見落とすか
文字列型の数値変換失敗「処理した」事実を報告するが、結果の妥当性を自ら検証しない
DateTime型のモデル混入エラーが出ないため異常として認識されない
suumo_codeの定期更新仕様ドメイン知識(サイトの仕様)を持っていない
募集期間「中央値4日」の異常「4日は短すぎる」という現実感覚がない
R²=0.836の見かけ上の高精度高いR²を「良い結果」として肯定してしまう傾向がある

AIは与えられた指示を忠実に実行する。しかし「この結果はおかしい」「この集約方法は現実と合っているか」というドメイン知識に基づいた懐疑心は、人間が持ち込まなければならない。

データサイエンスは「AIに任せれば終わり」ではない。むしろAIを使えば使うほど、人間側の「問いを立てる力」と「結果を疑う目」が重要になる。


まとめ:前処理は「分析の9割」である

今回の分析で前処理に費やした時間は、モデル構築や解釈の時間をはるかに超えていた。そしてその前処理の品質が、最終的な結論の正否を決定的に左右した。

前処理の重要なチェックポイントをまとめる。

  1. 型変換の結果を必ず検証する(変換後のNaN率、値の範囲を確認)
  2. 除外すべき列を明示的にリストアップする(ID列・日付列・生テキスト)
  3. 集約結果が現実と整合するか確認する(「募集期間4日」は現実的か?)
  4. ユニークキーの定義を慎重に行う(データソースの仕様を調査する)
  5. VIFで多重共線性を確認する(変数を増やす前に必ず確認)
  6. 高いR²を盲信しない(集約バグや過学習の可能性を疑う)

「正しいデータセット」なしに「正しい結論」はあり得ない。前処理はデータサイエンスの花形ではないかもしれない。しかし、それが全ての土台だ。


データ分析のご相談はhoscmへ

「自社のデータを分析したいが、どこから手をつければいいかわからない」「AIを使ってみたが正しい結果が出ているか不安」——そんなご相談を承っています。

データの収集設計から前処理・モデル構築・結果の解釈まで、一貫してサポートします。まずはお気軽にご相談ください。

 hoscm サービスサイト

賃貸市場の「本当の家賃トレンド」をデータで暴く――5ヶ月間(2025年11月〜2026年3月)・1,365物件の分析から見えたこと

本記事は、AI連携でどこまで自動化できるかを検証したものです。記事の中身の妥当性については十分には検証できていませんが、作業過程で検証を何回か繰り返しています。元データの取得方法は過去記事を参考にしてください。では、以下がClaude codeを使って解析、生成した分析結果の記事です。


「最近、家賃が上がっている気がする」――そう感じている人は多いだろう。物価高騰が続く中、賃貸市場にも影響が出ているという報道は絶えない。では実際のところ、家賃は本当に上がっているのか。

筆者はある地域の賃貸物件データを約5ヶ月間・38時点にわたってスクレイピングし、統計的な手法で「本当の家賃トレンド」を分析した。単純な平均値の変化ではなく、物件の条件(広さ・築年数・設備など)を揃えた上で比較するという、ヘドニック価格指数的なアプローチを用いた。結果は、多くの人の直感を裏切るものだった。


データの概要

  • 収集期間: 約5ヶ月(38時点)
  • 生データ: 8,592レコード
  • 分析対象物件数: 1,365件(重複・外れ値除去後)
  • 物件タイプ: 賃貸アパート・マンション・一戸建て(ある地方都市周辺)

注目すべきは、大手賃貸サイトは同一物件の掲載コードを定期的に更新する仕様があることだ。そのまま集計すると同じ物件が複数カウントされてしまう。この問題を解決するため、Union-Find(素集合データ構造)というアルゴリズムを用いて同一物件を連結し、正確な集計を実現した。


発見①「家賃は上がっていない」――条件を揃えると見えた真実

まず、シンプルに取得日ごとの平均家賃をプロットすると、月▲380円という下落トレンドが確認された(p<0.001)。「家賃が下がっているじゃないか」と思うかもしれない。

ところが、物件の条件(広さ・築年数・設備・階数など44変数)を重回帰モデルで除去した後の「残差」で同じ分析をすると――

トレンド: 月▲106円、p値=0.190(統計的に有意でない)

つまり、条件を揃えると家賃変動はゼロに近い。物価高騰の影響は、少なくともこの地域・この期間においては賃貸市場には波及していなかったということだ。

生データで見えていた「下落トレンド」の正体は、安価な物件の大量新規掲載によるミックス効果だった。


発見②「2月の急落」は本物か

データを眺めていると、2月上旬に平均家賃が突如▲1,500円近く急落する場面があった。

要因金額
物件ミックスの変化(安価物件の大量掲載)▲1,000円
本物の需要緩和(条件考慮後も残る下落)▲500円
合計▲1,500円

急落の3分の2はミックス効果で、残り3分の1が実態のある需要緩和だった。このような「見かけの変動」と「実態の変動」を区別するには、条件考慮済みの分析が不可欠だ。


発見③「1月は割高、2月以降は割安」という季節性

条件考慮済みの残差を月別に見ると、明確な季節パターンが浮かび上がった。

時期残差(条件考慮済み)解釈
11〜12月±500円程度安定
1月+1,000〜+2,000円需要ピーク・割高
2月以降▲400〜▲800円需給緩和・割安

1月は引越しシーズン前の駆け込み需要で、同じ条件の物件でも約1,000〜2,000円高く成約されていた。逆に2月以降はその反動で割安感が出ている。

賃貸を探すなら、1月ではなく2〜3月以降の方がコスト効率が良いという示唆が得られる。


発見④ 家賃を決める最強の変数は「追焚」だった

重回帰分析で44変数を投入したところ、最も家賃との相関が高かった変数は意外にも「追焚(お風呂の追い焚き機能)の有無」(相関係数r=0.670)だった。

順位変数相関係数
1追焚設備あり+0.670
2専有面積(㎡)+0.651
3総戸数(棟の規模)-0.584
4仲介手数料額+0.526
5礼金+0.459

追焚が最強の予測変数になった背景には、地域の入浴文化や生活習慣が影響している可能性がある。また、「大規模物件(総戸数が多い)ほど家賃が低い」という結果は、管理コストの規模の経済を反映していると考えられる。


発見⑤ 「2LDKは割高、1LDKは割安」という構造的価格差

間取り別に条件考慮済みの残差を分析すると、興味深い構造が浮かび上がった。

間取り残差解釈
2LDK+1,000〜+3,000円一貫して割高
1LDK▲1,000〜▲2,000円一貫して割安
1K季節性あり(1月に割高)需要変動大きい

同じ面積でも、2LDKはファミリー向けの需要プレミアムが乗っており割高になる傾向がある。一方1LDKは供給過多気味で割安。カップルや二人暮らしなら1LDKも候補に入れると費用対効果が高い。

賃貸市場の家賃トレンド分析グラフ(条件考慮済み)
賃貸市場の家賃トレンド分析グラフ(条件考慮済み)

分析に使ったモデルの精度

最終的に採用したモデル(重線形回帰・44変数+交互作用2項目)の精度は以下の通り:

指標
R²(決定係数)0.750
調整済みR²0.699
RMSE4,506円
VIF>10(多重共線性)なし

実際の家賃と予測値の誤差が平均±4,500円程度に収まっており、賃貸物件の価格モデルとして実用的な精度が得られた。


まとめ:賃貸市場の「見えない構造」

今回の分析から得られた主要知見をまとめる。

  1. 物価高騰は賃貸家賃に波及していない(少なくともこの地域・この期間)
  2. 家賃の下落トレンドは物件ミックスの変化が原因で、実態は横ばい
  3. 1月は割高、2月以降は割安というサイクルが存在する
  4. 追焚・専有面積・大規模物件の規模が家賃を最も強く説明する
  5. 2LDKは割高、1LDKは割安という構造的価格差がある

賃貸物件を探す際、これらの知見を活用することで、より合理的な意思決定ができるだろう。


分析の補足・免責事項

本分析は特定の賃貸情報サイトのデータをスクレイピングして実施したものであり、市場全体を代表するものではない。また、「掲載から消えた = 入居決定」という推定には不確実性が伴う。分析期間が約5ヶ月と限られているため、年間の季節変動を完全には捉えられていない点にも注意が必要だ。

本記事の分析コードはPython(pandas・scikit-learn・statsmodels・scipy)を使用して作成した。


どうだろうか、素人が使いこなすには難しいかもしれないが、分かっている人が分析する分には効率的な作業を行える。課題は本当にわかっているか、ちゃんと検証できているかだろう。 この資料を生成するまでの過程は、別の記事にしてみよう。


関連記事

IT駆使(hsBox1.3活用)、テクノロジー駆使で水濡れ原因を謎解き! メカニズム新説へ?

以前に書いた「映像監視への、漏水状況の追加監視について2026/1/12」の記事で、示した漏水の謎についてほぼ事象を確認できたのでここでまとめておく。

写真の上部の中央やや右よりに、先の記事で書いた「水分検知シート」をテープ状に切って張り付けた。張り付けたのは昨年末で、上の画像の通り1/30にはまだ白色で水濡れは発生していなかった。
 しかし、先日確認したところ、明らかな水濡れが発生したことを確認した。

tape20260319

seat
水検知シート

赤色に変色しているところが、水で濡れたところである。この変色の状況から、いくつかの状況が読み取れる。それについては後で整理することにする。


水濡れ原因の検証・推定

さて、今回水濡れが発生した原因は、先に推定した漏水・吐水だったのだろうか?それを確認するため、水濡れが発生した時間を特定し、そこで何か起こっていたのかを確認しよう。

目視で、水濡れが発生していなかった記憶は、1月末くらいである。そこで、3月以降の画像記録をたどってみた。3月13-14日を除いて3月1日から3月19日まで、欠落なく10分毎の画像キャプチャができているようだ。3月13日13:40から3月14日18:20の間はキャプチャが欠損している。(※キャプチャ失敗に1日程度気が付かなかった。そこで対策としてhsBoxのステータスモニター機能を使うことにした)

3月14日18:30時点の画像では、水濡れが発生していたようである。3月13日13:30時点の画像はカメラが横倒しで対象個所を撮影できていない。遡るとカメラが横倒しになったのは3月13日10:20から10:30の間であった。 そして、3月13日10:20時点画像ですでに水濡れが発生していたと判断できた。どうも、水濡れが発生したのは2月の何処かのようだ。

2/1 ◎ 2/7◎ 2/9◎ 2/10 17:00〇 2/11 8:00× 2/11x 2/12 12:30× 2/15x 3/1×

確認の結果、2/10 17:00と2/11 7:10の間に発生したようだ。

つぎは、2/11 20:00時点の画像である。

yoru
2/11 20:00

この時点で、水濡れはすでに発生しているにもかかわらず、この画像では確認できない。暗視状態(白黒)の画像では、赤外光で撮像するため赤くなっていても暗くうつることはない。つまり、暗がりでは水濡れを検知テープの状態から判別することができないのである。

遡って、暗視画像を確認すると、2/10 5:50時点では撮影範囲内には特に変化は見られなかった。しかし6:00時点で上部に水のシミが映り込み、7:00まで1時間かけてじわじわ広がっていく様子が確認できた。そのシミの範囲と、変色部分の位置は一致しており、このシミが水であったことを確認できた。

2/10 6:00ころの天気が何であったか確認してみよう。気象庁記録では、前日夜から午前10時ころまで雨であった。気温は7度前後、風速は2m/sである。この場所は、上にベランダが張り出しているので雨がかからないので、これまでコンクリート奥から漏れ出てきたと推定していた。しかし、他の仮説も立てる必要があるかもしれない。

また、画像:tape20260319 これは、2/11時点の濡れ状態から、さらに1回ほど濡れが追加されているように見える。
 その1つは、小さな霧状の水滴がついたように見えることによる。この小さな点は、上から貼り付けたメンディングテープの部分には発生しておらず。霧吹きのように振りかけられたように見える。上と同様に、発生した日時と、その時の天候を確認した。

3/16◎ 3/17◎ 3/18◎ 3/19×

こちらも夜間に発生したようだ。3/18 17:00から3/19 7:00の間で、細かい赤い点が発生し、メンディングテープの位置がくっきりと見えるようになっている。
この日も3/18から3/19にかけて雨が降っており気温は11℃程度、風速は3m/sちょっとだった。2/11とは風向きが逆で吹き込んでくる方向だった。霧雨状の水滴がついたと考える。

この状況から、一見濡れなさそうなこの面も雨の降り方と風速と風向きしだいで濡れることがあることが確認できた。

水濡れの原因調査は、水検知シート+hsBoxで

水検知シートだけだと濡れたことを確認できただろうが、いつ濡れたか、その原因の特定までは難しかったかもしれない。定点観測的に監視カメラでキャプチャし続けたことで、濡れたタイミングを特定して、ほぼ確実な原因を推定できた。

▼新説:構築 → 次の分析へ

残念ながら新たに確認できたのは、一見濡れないと思われた場所も状況によっては濡れる可能性があることが示されたことにとどまる。新しい仮説を組み立てるほどの状況にはない。とにかく、今年の秋から冬にかけては雨が少なかった。これが、今回の調査が進まなかった、つまり問題事象が発生しにくかったことに効いているかもしれない。


関連記事

MRAM・スピントロニクスは何を変えるのか ― AI時代が求める「新しい記録デバイス」| ストレージ再考 第8話

第7話では、ストレージやメモリといったデバイスの特性に合わせて、
システム設計を最適化していく必要がある
、という話をしてきた。

しかし、いま起きている変化は少し違う。

AIは、従来の記録デバイスでは成立しない要求を突きつけ始めている。

その要求に押される形で、これまで主流になりきれなかった技術が、
再び注目され始めている。

その代表が MRAM(磁気メモリ) である。

MRAMとは何か:電気ではなく「磁気」で記録する

従来のメモリやストレージは、基本的に「電荷」で情報を記録している。

  • DRAM:電荷を保持(揮発)
  • NANDフラッシュ:電荷を閉じ込める(不揮発)

これに対してMRAMは、

磁気(スピンの向き)によって情報を記録する

という全く異なる原理を持つ。


▼解説動画
https://www.youtube.com/watch?v=VRJ7xYPMfGA


■ もう少しだけ技術寄りの話(軽く触れる)

MRAMのコアは以下の構造にある:

  • MTJ(Magnetic Tunnel Junction)
  • スピントロニクス
  • STT-MRAM(Spin Transfer Torque)

詳細は以下を参照:


MRAMは新しい技術ではない

MRAMは最近突然現れた技術ではない。

むしろ長年研究されてきたが、主流になれなかった技術である。

その理由は単純だ:

  • 投資が集中しなかった
  • 市場が形成されなかった
  • 既存技術(NAND・DRAM)が強すぎた

■ 技術の優劣ではなく「投資構造」

  • NAND → 巨大市場 → 投資集中 → 爆発的進化
  • MRAM → ニッチ → 投資限定 → 緩やかな進化

しかし今、状況が変わり始めている。

AIという新しい要求が、投資の方向を変え始めている。


MRAMはすでに使われている(ただし“見えない場所”で)

MRAMは未来の技術ではない。
すでに実用化され、いくつかの分野で使われている。

ただしその使われ方には特徴がある。


■ 主な用途

① 組み込みメモリ(SoC内)

  • ファームウェア保存
  • セキュリティ領域
  • 設定データ

② ログ・高頻度更新領域

  • イベントログ
  • 状態保存
  • トランザクション記録

③ キャッシュ・バッファ

  • ストレージ制御
  • ネットワーク機器

④ 車載・産業機器

  • 電源断耐性
  • 高信頼性要求

■ スマートフォンでの利用

近年では、SoC内部の一部領域に組み込みMRAM(eMRAM)が使われ始めている。

例:

  • セキュア領域
  • 制御用データ保持

参考:


■ 重要なポイント

MRAMは現在、
「小さいが重要な場所」に使われている


なぜそこに使われるのか:設計思想が変わるから

特に重要なのが、高頻度更新領域の置き換えである。


■ 従来(フラッシュ)

  • 書き込み回数に制限
  • 書くほど劣化
  • 書き込み最適化が必要

👉 「なるべく書かない設計」


■ MRAM

  • 高い書き換え耐性
  • 電源断でも保持
  • 書き込みコストが低い

👉 「普通に書いていい設計」


これは単なる性能向上ではなく、設計思想の変化である。


AI時代が突きつけた新しい制約

ここで本題に入る。

AIは従来のシステムとは異なる要求を持つ。


■ ① 大量のデータを「近く」に置きたい

  • モデル
  • 中間データ
  • キャッシュ

■ ② とにかく書き続ける

  • 学習
  • ログ
  • 状態更新

■ ③ 電力制約が限界に近づいている

  • データセンターの電力問題
  • 発熱
  • 冷却コスト

AIは「計算」ではなく「配置と電力」の限界を突きつけている。


MRAMが注目される理由:電力構造の違い

MRAMは「電力を使わないメモリ」ではない。

「使わないときに電源を完全に止められるメモリ」である。


■ 比較

技術特徴
DRAMリフレッシュで常時電力消費
NAND書き込み時に高エネルギー
MRAM状態保持に電力不要

■ 重要なポイント

多くのデータは「使われていない時間」の方が長い


👉 その時間の電力を削減できることが、本質的な価値である。


MRAMの現状:主役ではないが確実に入り込んでいる

現時点のMRAMは:

  • コストが高い
  • 容量が小さい
  • 主記憶やストレージの代替にはならない

しかし、効果的なポイントに限定して使われ始めている。


■ 今後の分岐点

DRAMレベルまでコストと容量が近づいたとき、状況は変わる可能性がある


ただし重要なのは:

それは単なる価格競争ではなく、
AIの要求と一致したときに起こる変化である

MRAM
MRAM

未来:ノイマン型の限界とその先

現在のAIは、

  • CPU / GPU / TPU
  • メモリ分離構造

👉 ノイマン型コンピュータの延長上にある


しかしその先では:

記録と計算が一体化した構造へと進む可能性がある


■ 脳との類似性

人間の脳は:

  • 高密度
  • 低消費電力
  • 再学習による適応

例えば:

  • 脳卒中後のリハビリ
    → 完全修復ではなく再構成

■ コンピューティングも同じ方向へ

壊れても動くのではなく、壊れても適応する


MRAMのようなメモリは、

  • 書き続けられる
  • 状態を保持できる
  • 局所配置できる

👉 この構造と非常に相性が良い


結論:求められているのは「壊れないこと」ではない

最後に、この回の結論をまとめる。


これからの記録デバイスに求められるのは、
壊れないことではなく、適応できることである。


MRAMはまだ主役ではない。

しかし、

  • AIの要求
  • 電力の制約
  • 配置の問題

これらが交差したとき、

ストレージとメモリの前提そのものを変える可能性を持っている。


関連記事

なぜ壁は膨らんだのか?IT駆使で迫るコンクリート劣化の原因調査 ~2から6週間分 中間まとめ ~

昨年12月に本格着手して、さまざまな角度でITを駆使しながら進めてきています。以下は、ここまでに確認できた結果の要点の書き出してまとめます。 当初計画した調査環境の整備が整ってから2週間ほどしか経っていませんが、当初の仮説とは少し異なる新たな仮説を立てています。

ATOMCAM2撮影、画像キャプチャ(コンクリート劣化)


カメラでの定点観測は2025/12/23より運用開始: 上のとおり、キャプチャ開始時点から、後述のAEセンサー、温度センサー(熱電対)を追加しています。また、上右よりの2本のテープ状のものは水濡れ検知シールです。水に濡れると下の写真のように赤色に変わり乾いても赤のままなので、水が垂れてきたことがあったかどうか分かります。今のところ、水は出てきていません。同様に熱電対を入れた塩ビ管内にも。このシールを入れたので、壁の内側で結露が発生したかどうかを判別できるでしょう。

mizu
水検知シール

MCR-4TCでの温度測定

温度測定
温度測定

今のところ気温が一瞬0℃になりましたが、コンクリート裏はそこまでは下がっていません。気温が下がってコンクリート内の温度が連動するところもあれば、連動しないところもあります。単純に外気温だけがコンクリート裏側の温度に効いているわけではないようです。

 また、場所によって外気温の影響が異なるように見えます。

AE測定

[ EEPROM Data Analysis Report ]
Type | Date Time (UTC/Local) | Millis (raw) | Details
--------------------------------------------------------------------------------
SYNC | 2026-01-14 13:25:51 | 325120 | Time Synchronized (Unix:1768364751)
DATA | 2026-01-14 14:25:45 | 3919360 | CH0:0, CH1:19, CH2:0, CH3:0
DATA | 2026-01-15 06:45:30 | 62704128 | CH0:0, CH1:8, CH2:0, CH3:0
DATA | 2026-01-16 00:26:07 | 126341632 | CH0:0, CH1:8, CH2:0, CH3:1
DATA | 2026-01-16 06:11:56 | 147090432 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-16 07:11:10 | 150644736 | CH0:0, CH1:0, CH2:3, CH3:7
DATA | 2026-01-16 10:10:08 | 161382912 | CH0:0, CH1:16, CH2:0, CH3:0
DATA | 2026-01-16 10:16:08 | 161742848 | CH0:0, CH1:7, CH2:0, CH3:0
DATA | 2026-01-16 10:57:17 | 164211968 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-16 12:33:41 | 169995520 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-16 12:47:02 | 170796544 | CH0:0, CH1:7, CH2:0, CH3:0
DATA | 2026-01-17 16:21:30 | 270064640 | CH0:0, CH1:11, CH2:1, CH3:1
DATA | 2026-01-18 01:01:31 | 301265664 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-18 05:01:11 | 315645696 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-18 12:22:00 | 342094336 | CH0:0, CH1:1, CH2:6, CH3:6
DATA | 2026-01-19 00:38:54 | 386308608 | CH0:0, CH1:45, CH2:0, CH3:0
DATA | 2026-01-19 06:01:51 | 405686016 | CH0:0, CH1:8, CH2:0, CH3:0
DATA | 2026-01-20 12:54:55 | 516869376 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-20 18:41:19 | 537653504 | CH0:0, CH1:7, CH2:0, CH3:0
DATA | 2026-01-22 01:14:26 | 647640576 | CH0:0, CH1:7, CH2:0, CH3:3
DATA | 2026-01-25 12:50:54 | 948628992 | CH0:0, CH1:9, CH2:0, CH3:2
DATA | 2026-01-29 15:48:20 | 1304874752 | CH0:0, CH1:23, CH2:3, CH3:4
RSET | 2026-01-30 12:55:41 | 0 | System Boot / Reset Detected
SYNC | 2026-01-30 12:58:24 | 162304 | Time Synchronized (Unix:1769745504)
--------------------------------------------------------------------------------
Analysis Finished.

顕著なAE信号検出はなさそうで、たまに何かを検出してはいますが、ほとんど発生していないようです。1/16がちょっと多い感じがしますが、今のところ顕著な問題は起きていなさそうです。

コンクリート壁穿孔工事関連

熱電対を設置した状況からそれぞれの場所ごとにコンクリート壁と、壁内の土の間に隙間があり、場所によってその隙間が、約5mmから約48mmと大きなばらつきがあるように見えます。

中間まとめ 

 今のところ、新たに見えた事象は、コンクリート裏に隙間があるようにみえるということです。その隙間と、コンクリート奥の温度に関連性があるように見えます。つまり、隙間が大きいほど、外気温が下がっても温度が下がりにくそうということです。

 それとは別に隙間に関係なく、タイミングによってどの面の温度が低くなるのかが変わりそうです。

温度測定が終わる4月頃に、熱電対をとおした塩ビパイプを取り出して、内視鏡カメラで壁の裏側の隙間の状態を調査しようと考えています。

今年はほとんど雨降っておらず、乾ききっている感じなので問題事象は発生しにくそうです。

新たな仮説

これまでの仮説はすべての面が均等にアイスレンズが成長するように考えていましたが、片面だけ発生するケースもありそうな感触です。つまり、東面と南面で交互に発生し隙間が広がったり、逆に押されてほとんどなくなったりすることが繰り返されたのかもしれません。

さらにここまでの仮説で将来発生することが予想されていた西面北寄りでの事象発生ですが、下の写真のように新たに発生していることを確認しました。

crack
ひび割れ

ここまでの仮説を補強する材料と言ってよいでしょう。 

関連記事

hsBoxで、ATOM Cam 2の映像をキャプチャで発生した問題と課題と対処策について

約1か月連続キャプチャできていましたが、キャプチャできなくなる事象が発生したのでその原因調査と対策をしていきます。そして恒久対策に向けた対策案も検討して、以前に公開したツールの今後の強化項目にしていきます。

cam
cam

調査 その1

まず、状況整理ですが、キャプチャは、1回のcron起動で、3つのATOM CAM2を順にキャプチャしてファイル保存するように実装していました。一番最初にキャプチャするカメラ画像は10:00にキャプチャしたものが最後で、そのほかの2つは、14:40が最後でした。つまり、1つ目のキャプチャに失敗しても2つめ以降のキャプチャはできていましたが、その後3つともキャプチャできなくなっています。運用点検のため、hsBoxを再起動したタイミングあたりでキャプチャできなくなっていたようです。
 まずは、切り分けしていくためウォークスルー方式で問題となりそうなところを順にチェックしていきます。まずは、NASのマウント状況を確認しました。マウントはできており問題ありませんでした。
次に、ATOM CAM2のIP探索の確認です。 スマホでATOM CAM2のIPを確認して、そのIPが取れているかを、arpコマンドで確認します。 すると、3台のカメラともにMACが取れていないことが判明した。

暫定対処 その1

それぞれの、IPにpingを打ってみます。 それぞれ、反応がありMACがとれました。これにより、14:40が最後だった2台のカメラのキャプチャが復旧しました。
 残りの1台は、NAS上へのフォルダ作成には成功したものの、画像ファイルがまだ置かれていない状況になっています。

根本対策 その1

 今後の強化策です。 既存実装でもブロードキャストでpingを打つ実装を入れていますが、ブロードキャストではarpに反映されないのが問題のようです。そこで、MACが取れなかったとき、DHCPで割り振られる範囲に対して明示的に個別にpingを打つ実装を追加することにします。

調査 その2

まだ復旧できていない1台分のカメラについて調査を行います。

実装を、最後にキャプチャできたタイミングで何かあったかを確認してみましたが、特に問題はないようです。手動で、FFMPEGコマンドを実行すると、次の結果が返ってきました。

> python3 .\cap_once.py
FFmpeg failed: Command '['ffmpeg', '-rtsp_transport', 'tcp', '-i', 'rtsp://**:***@192.168.*.*/live', '-ss', '00:00:01', '-vframes', '1', '-q:v', '2', '-y', '\\\\192.168.*.*\\***\\2026-01-25\\20260125_124510.jpg']' timed out after 20 seconds

タイムアウトしていますが、pingは通っている。 前のセッションが残っていて接続待ちの状態になっている?のかという説はあるが、 スマホからの確認では動作している。 スマホでの画像表示と、rtspは別動作なので、rtsp側だけの問題(セッション残留?)の可能性がある。

暫定対処 その2

セッション残留の問題だと仮定して、rtspの再起動での復旧を試みる。スマホでの操作で一度「PCで再生」をオフにして再起動してみる。
 この操作を行うとrtsp用のユーザとパスワードが更新された。新たなユーザとパスワードを反映すると、キャプチャに成功し、 自動キャプチャは復旧した。

問題の原因は、残留セッションとみてよいようだ。 これは、ATOM CAM2側で何とかしてほしいが、 録画の都合(解像度の高い録画をしたい)から古いバージョンのファームウェアを使用している。このため最新パッチを適用することができず、ATOM CAM2側での”残留セッション”の改善は望めない。そこで、hsBox側での別の改善策を取ることにする。

その2の問題の 改善策案

”残留セッション”問題の対処方針は、上の”暫定対処”の問題を早期検知し、できるだけ手間をかけずに復旧できるようにするというものである。

 復旧手順を明記した内容での問題検知通知を出し、スマホ操作で「PCで再生」を再起動し、あらたなユーザ名・パスワードをGUI設定画面で更新するというものである。
とりあえず今時点での簡便な復旧策はこの辺でしょう。

他に良い案があれば、コメント投稿をお願いします。


関連記事

https://mic.or.jp/info/2026/01/30/cam-4/

hsBox1.3で、太陽光発電機器故障を検知して通知してみよう

2025年12月22日で太陽光発電 通知メールがなくなる件。 代替として自前で収集した情報をLINEに通知してみた」をさらに高度化を検討してみた。
 過去の実装(太陽光発電システムの故障判断アルゴリズム(その1))で、故障検知を高精度(6年間で5回検知・誤検知なし。1件はパワコンブレーカーダウン、4件はデータアップロードネットワークのダウン)で実装できていましたが、さきのソーラーフロンティアのサービス終了でこの検知システムが使えなくなりました。そこで、新たな検知システムの実装を、先に公開してシステムを強化して、検討してみます。

 ちなみに、公開済みの発電量収集代替実装では、従来のソーラーフロンティアの発電量との誤差は±2%程度なので、3か所ともに発電量収集代替実装を導入して、既存の検知システムにデータを流し込めば高精度での判定を継続できるようになります。しかし、通常、太陽光発電システムを複数保有しているほぼないので、この方式はほぼ使えません。そこで、ここでは1システムだけであっても判定することができる仕組みを検討してみます。

SolarAlert_
太陽光発電システム 故障通知

故障検知実現の設計方針

最終的には、人が判断するが、できるだけ判定精度を上げたい。過去の高精度判定システム構築での知見を反映して設計する。ポイントは次の通り。
・快晴時の発電量は安定しており、きれいなカーブを描く。
   →判定はこの部分のデータを使う。
・雲がある晴れの日のデータは変化が激しい。
・発電量の小さい日の発電量のばらつきが大きい。
   →判定には使用せず、参考値として通知する。
・四季の変化での発電量の補正が必要
→過去実装では、実績のピークデータをもとに関数化して組み込み
想定発電量と比較し、判定するかどうかを切り分けした。
・外部情報として最も近い気象庁観測点のデータを使用する。
・太陽光パネルの様々な設置場所ごとの違いによる想定発電量を単純な計算式ではなく学習方式を採用する。

使用するデータは、以前に公開したParquet形式で保存したものとする。

実装例 


import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
import sys
import argparse
import requests
from bs4 import BeautifulSoup

# ==================== 設定エリア ====================
NAS_PATH = r"/mnt/nas/PowerData" #★
MODEL_PATH = os.path.join(NAS_PATH, "learning_model.parquet") # 学習データ保存先
IFTTT_WEBHOOK_URL0 = "https://maker.ifttt.com/trigger/{event}/with/key/{your_key}"
IFTTT_EVENT_NAME = "message_to_myline" #★
your_key = "************" #★
IFTTT_WEBHOOK_URL = IFTTT_WEBHOOK_URL0.replace("{your_key}", your_key)

# 判定しきい値 (期待値の50%以下が2時間続いたら故障疑いなど)
THRESHOLD_RATIO = 0.5 #★
# ====================================================

def send_line_message(title: str, body: str, timestamp: str = "-"):
payload = {"value1": title, "value2": body, "value3": timestamp}
url = IFTTT_WEBHOOK_URL.replace("{event}", IFTTT_EVENT_NAME)
try:
response = requests.post(url, json=payload, timeout=10)
return response.status_code == 200
except Exception as e:
print(f"LINE通知失敗: {e}")
return False

def get_jma_sunshine_hourly(target_date: datetime):
"""気象庁HPから10分ごとの日照時間を取得し1時間単位(0.0-1.0)で返す"""
url = (f"https://www.data.jma.go.jp/stats/etrn/view/10min_s1.php?"
f"prec_no=**&block_no=****&year={target_date.year}&month={target_date.month}&day={target_date.day}&view=p2") #★
try:
res = requests.get(url, timeout=10)
res.encoding = res.apparent_encoding
soup = BeautifulSoup(res.text, 'html.parser')
rows = soup.find_all('tr', class_='mtx')
sunshine_10min = []
for row in rows:
cols = row.find_all('td')
if len(cols) > 10:
val = cols[9].text.strip() # 日照時間の列
sunshine_10min.append(float(val) if val else 0.0)
# 1時間(6個)ごとに合計(最大1.0)
return [sum(sunshine_10min[i:i+6]) for i in range(0, len(sunshine_10min), 6)]
except Exception as e:
print(f"気象庁データ取得失敗: {e}")
return [None] * 24

def update_learning_model(target_date: datetime, hourly_actual: list, hourly_sun: list):
"""日照が良い時のデータを『快晴時の正解』として学習モデルを更新"""
month = target_date.month
try:
if os.path.exists(MODEL_PATH):
model_df = pd.read_parquet(MODEL_PATH)
else:
# 初期モデル: 全月0で作成
model_df = pd.DataFrame(0.0, index=range(1, 13), columns=[f"h_{i}" for i in range(24)])

updated = False
for h in range(7, 18): # 発電時間帯のみ
# 日照率が0.9以上かつ、これまでの学習値より高い(または初データ)場合に更新
if hourly_sun[h] and hourly_sun[h] >= 0.9:
current_val = model_df.loc[month, f"h_{h}"]
if hourly_actual[h] > current_val:
model_df.loc[month, f"h_{h}"] = hourly_actual[h]
updated = True

if updated:
model_df.to_parquet(MODEL_PATH)
print("学習モデルを更新しました。")
except Exception as e:
print(f"学習モデル更新失敗: {e}")

def load_day_data(target_date: datetime) -> pd.DataFrame:
date_str = target_date.strftime("%Y%m%d")
file_path = fr"{NAS_PATH}/power_{date_str}.parquet"
if not os.path.exists(file_path):
raise FileNotFoundError(f"データが見つかりません: {file_path}")
return pd.read_parquet(file_path)

def main():
parser = argparse.ArgumentParser()
parser.add_argument("--da", type=int, default=0)
args = parser.parse_args()

base_date = datetime.now() + timedelta(days=args.da)
date_label = base_date.strftime("%Y/%m/%d")

try:
df = load_day_data(base_date)
# 1時間ごとの発電量(value1)リスト作成
df['hour'] = pd.to_datetime(df['timestamp']).dt.hour # timestamp列があると想定
hourly_actual = df.groupby('hour')['value1'].mean().reindex(range(24), fill_value=0.0).tolist()
except Exception as e:
print(f"データ読み込みエラー: {e}")
return

# 気象庁データ取得
hourly_sun = get_jma_sunshine_hourly(base_date)

# 学習モデルの更新
update_learning_model(base_date, hourly_actual, hourly_sun)

# 故障検知
is_anomaly = False
anomaly_details = []
if os.path.exists(MODEL_PATH):
model_df = pd.read_parquet(MODEL_PATH)
month = base_date.month

for h in range(8, 17): # 影の影響が少ない時間帯を重点チェック
sun = hourly_sun[h]
if sun and sun > 0.3: # ある程度の日照がある場合
expected = model_df.loc[month, f"h_{h}"] * sun
actual = hourly_actual[h]
if actual < expected * THRESHOLD_RATIO:
is_anomaly = True
anomaly_details.append(f"{h}時(実測{actual:.1f}kW/期待{expected:.1f}kW)")

# メッセージ作成
total_gen = sum(hourly_actual) * (10/60) # 10分データの場合の概算
status_msg = "✅ 正常" if not is_anomaly else "⚠️ 故障の疑い"

message = f"⚡️ {date_label} 発電実績 ({status_msg})\n" \
f"総発電量:{total_gen:.2f} kWh\n"

if is_anomaly:
message += "\n【異常検知】\n" + "\n".join(anomaly_details)
send_line_message("太陽光発電アラート", message, "ALERT")
else:
send_line_message("太陽光発電データ", message, "OK")

print(message)

if __name__ == "__main__":
main()

上の実装は、あくまでもサンプルです。 特に”★”印のある行は環境に合わせて修正してください。

このコードで検証していますが、かなり過敏で誤検知が多いです。
閾値調整で、精度を上げることになると思いますが、過去実装での故障判定ロジックを超える精度にはならなさそうです。 誤検知をゼロにするには、十分に学習させたうえで、1日中快晴だった日のデータでのみで判定する必要がありそうです。

他に改善案などあればコメントをお願いします。

関連記事

 

AE測定、長期ロギングシステム構築 (その7) 運用開始編

前回までで、AEイベントを収集するセンサーから信号を取り出し、Arduino複数チャンネルのAD変換してイベントをとらえる事前検討完了しました。ここでは、長期ロギングに向けて最終的な統合をしていきます。

ArduinoIDE
ArduinoIDE

 長期ロギング用の実装を事前準備していたのですが、実際にロギングを開始してみるといくつもの新たな課題を確認しました。それらの課題はつぎのとおりです。

  • Flashへのイベントデータの記録ができない。
  • EEPROMなどのデータ回収でavrdude.exeを使うとリセットが発生する
  • シリアルモニタを使うとON/OFFでリセットが発生する。
  • 閾値を超えていないイベントが記録される。
  • ロギング用プログラムを起動した直後にイベントが記録される。
  • 隣のチャンネルの信号の影響を受けているように見える。

これらの課題について、以下のように検討して、対処していきました。

Flashへのイベントデータの記録ができない。

できるだけ多くのイベントデータを記録できるようにEEPROMではなく、Flashに記録する方針でしたが、動作検証するとこのArduinoは、細工しないとFlashに記録できないことが判明しました。 次の件の課題もあり、Flashへの記録はあきらめて、EEPROMを目いっぱい活用することにしました。イベントデータは1ブロック8byteに圧縮する仕様としました。

EEPROMなどのデータ回収でavrdude.exeを使うとリセットが発生する

 avrdude.exeを使うには、一度リセットしなければならないことが判明しました。検討の結果、データ回収、などのすべての運用に必要な機能を、イベントロギングと並行で処理できるように実装することにしました。

シリアルモニタを使うとON/OFFでリセットが発生する。

シリアルモニタは、ON/OFFで、リセット信号を送信する仕様になっているようだ。シリアルモニタでデータ回収することも検討していたが、データを吸い上げる実装もロギングツール内に実装することにしました。

閾値を超えていないイベントが記録される。

これはいくつかバグや後述の項目の問題が関わっていました。
まず、最初の実装では、閾値判定したデータそのものがイベントのそのチャンネルでの最大値に反映されておらず、さらに閾値を越えた後に配列データの0クリアを行う構造になっており、閾値越え検出から本計測開始までに余計な時間がかかっていました。そこで、サンプリングの高速化と閾値越えから本計測開始までの時間を最小化できるように、簡易オシロで採用したリングバッファ構造を採用しました。

ロギング用プログラムを起動した直後にイベントが記録される。

リセット直後(ほぼ同時に)に閾値を越えていないイベントが複数回検出された。毎回検出されるわけではないが、記録量引きの少ないEEPROMを無駄に消費するので、これの対策を検討した。これが前段の件と関連して発生していた。どうもAD変換のレジスタにリセット直前の電位が残っていることがあるようだ。そこで、リセット直後の約1秒間はイベント情報をEEPROMに書き込まない仕様に変更しました。

隣のチャンネルの信号の影響を受けているように見える。

隣のセンサーに影響が出ないように衝撃を与えた時に隣のチャンネルの信号の影響を受けているように見えた。当初、AD変換で直前のチャネルの電位の影響を受けて信号を拾っているのではないかと調整を試したが改善しなかった。 顧みると実験室で検証したときよりより多くの隣のチャンネルの影響を受けているようだった。試しに実験室内での計測したチャンネル切り替えで隣のチャンネルの影響を受けていなかったプログラムに戻して検証すると確かに、実験室の時より隣のチャンネルの影響が大きいことを確認した。この結果から、AD変換チャンネル切り替えの問題ではなく、物理的な信号ラインの クロストークが発生していると判断した。 フィールドへ配置する際に配線を束ねており、実験室に比べて電磁的クロストークが発生しやすい状況になったと推測されるそこで、信号ライン同士をできるだけ物理的に離すように調整した。 それでもある程度影響が残るので、可能な範囲で波形観測もできるように、リングバッファのほかに波形保存用のウインドウバッファを使う仕様に拡張しました。RAMではあるが、最後の2回については保存できるようにしました。

最終ロギング用 Arduinoスケッチ

#include <EEPROM.h>
// 2026/01/08 update
//
const int NUM_CHANS = 4;
const int W_NUM = 2; // ウインドウ数
const int DISPLAY_POINTS = 50; // 画面に表示したい総行数
const int PRE_TRIGGER_SAMPLES = 5; // そのうちの20%(過去分)
const int POST_TRIGGER_SAMPLES = DISPLAY_POINTS - PRE_TRIGGER_SAMPLES; // 残りの80%(未来分)

const unsigned long RECORD_WINDOW_MS = 1000;
const int EEPROM_MAX = 1024;
const int LIMITSC = 500; // シリアルチェックのサンプルカウント間隔
const int BUFFER_SIZE = 1100; // バッファサイズ (1.1KB)
const int WBUFFER_SIZE = W_NUM * NUM_CHANS * DISPLAY_POINTS; // ウインドバッファサイズ (0.4KB) 2回x4chx50点
const int THRESHOLD = 5; // 閾値 (8bit: 0-255) ※約2.5V


int currentAddr = 0;
bool isMonitoring = false;
uint8_t maxValues[NUM_CHANS];
unsigned long windowStartTime;
unsigned long windowTime[W_NUM];

int samplecount = 0;
int sw = 0; //ウインド書き込み位置

byte buffer[BUFFER_SIZE];
byte wbuffer[WBUFFER_SIZE];
int head = 0; // バッファ書き込み位置

void setup() {
Serial.begin(115200);
ADCSRA &= ~(bit(ADPS2) | bit(ADPS1) | bit(ADPS0));
ADCSRA |= bit(ADPS2);

findNextAddr();

// 起動記録 (01: Reset)
saveEventLog(0x01, 0);

Serial.println(F("SYSTEM_READY"));
}

void loop() {
// 1. シリアル通信処理 (バイナリ同期プロトコル対応)
if (samplecount > LIMITSC) {
handleSerial();
samplecount = 0;
} else { ++samplecount; }

// --- 1. 定常サンプリング ---
int val = analogRead(A0 + (head % NUM_CHANS));
buffer[head] = (byte)(val >> 2);

// --- 2. トリガー判定 ---
if ( buffer[head] > THRESHOLD) {
capture(head);
// 送信が終わったら head をリセットして、次のトリガーに備える
}

head = (head + 1) % BUFFER_SIZE;

/*
// 2. 監視ロジック
unsigned long now = millis();
if (!isMonitoring) {
for (int ch = 0; ch < NUM_CHANS; ch++) {
uint8_t val = analogRead(A0 + ch ) >> 2;
if (val > THRESHOLDS[ch]) {
isMonitoring = true;
windowStartTime = now;
maxValues[ch] = val;
break;
}
}
} else {
for (int ch = 0; ch < NUM_CHANS; ch++) {
uint8_t val = analogRead(A0 + ch ) >> 2;
if (val > maxValues[ch]) maxValues[ch] = val;
}
if (now - windowStartTime >= RECORD_WINDOW_MS) {
saveLog03(now);
for (int i = 0; i < NUM_CHANS; i++) maxValues[i] = 0;
isMonitoring = false;
}
}
*/

}

//ウインドウバッファへのコピーとイベント登録
void capture(int tIndex) {
unsigned long now = millis();
// 1. 未来分をサンプリング
for (int i = 0; i < (POST_TRIGGER_SAMPLES * NUM_CHANS); i++) {
head = (head + 1) % BUFFER_SIZE;
int v = analogRead(A0 + (head % NUM_CHANS));
buffer[head] = (byte)(v >> 2);
}

// 2. ウイドウコピー開始位置の計算
int startOffset = (tIndex - (PRE_TRIGGER_SAMPLES * NUM_CHANS) + BUFFER_SIZE * 2) % BUFFER_SIZE;
startOffset = (startOffset / NUM_CHANS) * NUM_CHANS;
windowTime[sw] = now;

// 3. ウイドウコピー
for (int i = 0; i < NUM_CHANS; i++) maxValues[i] = 0;
for (int i = 0; i < DISPLAY_POINTS; i++) {
for (int ch = 0; ch < NUM_CHANS; ch++) {
int idx = (startOffset + (i * NUM_CHANS) + ch) % BUFFER_SIZE;
int wi = (sw * NUM_CHANS * DISPLAY_POINTS + (i * NUM_CHANS) + ch) % WBUFFER_SIZE;
wbuffer[wi] = buffer[idx];
if (buffer[idx] > maxValues[ch]) maxValues[ch] = buffer[idx];
}
//

}


saveLog03(now);
sw = (sw + 1) % W_NUM;
}



// --- EEPROM記録関数 ---
void findNextAddr() {
currentAddr = 0;
while (currentAddr <= EEPROM_MAX - 8) {
uint8_t tag = EEPROM.read(currentAddr);
if (tag != 0x01 && tag != 0x02 && tag != 0x03) break;
currentAddr += 8;
}
}

// 種別01(Reset), 02(Sync)用
// Syncの場合は data に UnixTime を入れる
void saveEventLog(uint8_t type, uint32_t data) {
if (currentAddr > EEPROM_MAX - 8) return;
EEPROM.update(currentAddr + 0, type);
// データ4バイト分 (UnixTimeなど)
EEPROM.update(currentAddr + 1, (uint8_t)(data & 0xFF));
EEPROM.update(currentAddr + 2, (uint8_t)((data >> 8) & 0xFF));
EEPROM.update(currentAddr + 3, (uint8_t)((data >> 16) & 0xFF));
EEPROM.update(currentAddr + 4, (uint8_t)((data >> 24) & 0xFF));
// millis上位3バイト
unsigned long ms = millis();
EEPROM.update(currentAddr + 5, (uint8_t)(ms >> 24));
EEPROM.update(currentAddr + 6, (uint8_t)(ms >> 16));
EEPROM.update(currentAddr + 7, (uint8_t)(ms >> 8));
currentAddr += 8;
}

// 種別03(Measurement)用
void saveLog03(unsigned long timestamp) {
if ((currentAddr > EEPROM_MAX - 8) || millis() < 1025 ) return;
EEPROM.update(currentAddr + 0, 0x03);
for(int i=0; i<NUM_CHANS; i++) EEPROM.update(currentAddr + 1 + i, maxValues[i]);
EEPROM.update(currentAddr + 5, (uint8_t)(timestamp >> 24));
EEPROM.update(currentAddr + 6, (uint8_t)(timestamp >> 16));
EEPROM.update(currentAddr + 7, (uint8_t)(timestamp >> 8));
currentAddr += 8;
}

void handleSerial() {
if (Serial.available() > 0) {
uint8_t firstByte = Serial.peek();

// バイナリ同期パケット (0x02) の処理
if (firstByte == 0x02) {
uint8_t buf[6];
if (Serial.readBytes(buf, 6) == 6) {
uint8_t checksum = (buf[1] + buf[2] + buf[3] + buf[4]) & 0xFF;
if (checksum == buf[5]) {
uint32_t unixTime = ((uint32_t)buf[4] << 24) | ((uint32_t)buf[3] << 16) | ((uint32_t)buf[2] << 8) | buf[1];
saveEventLog(0x02, unixTime);
Serial.println(F("OK: Sync Success"));
} else {
Serial.println(F("Error: Checksum Mismatch"));
}
}
}
// テキストコマンドの処理 (既存のRead/Clear)
else {
char cmd = Serial.read();
if (cmd == 'D') { // Dump
Serial.println(F("START_DUMP"));
for (int i = 0; i < EEPROM_MAX; i++) {
uint8_t b = EEPROM.read(i);
if (b < 16) Serial.print('0');
Serial.print(b, HEX);
if ((i + 1) % 16 == 0) Serial.println();
}
Serial.println(F("END_DUMP"));
}
else if (cmd == 'C') { // Clear
for (int i = 0; i < EEPROM_MAX; i++) EEPROM.update(i, 0xFF);
currentAddr = 0;
Serial.println(F("OK: EEPROM Cleared"));
}
else if (cmd == 'P') { // Peek Get 波形取得
Serial.println(F("START_PDUMP"));
// 時間情報の出力 (ここはOK)
for (int i = 0; i < W_NUM; i++) {
Serial.print(windowTime[i]);
Serial.println(); // 改行があった方が良いかもしれません
}
// 波形データの出力
// wbufferは [ウィンドウ数 * ポイント数 * チャンネル数] の1次元配列
for (int i = 0; i < WBUFFER_SIZE; i += NUM_CHANS) { // NUM_CHANSずつ進める
for (int ch = 0; ch < NUM_CHANS; ch++) {
Serial.print(wbuffer[i + ch]);
if (ch < NUM_CHANS - 1) Serial.print(",");
}
Serial.println();
}
Serial.println(F("END_PDUMP"));
}
}
}
}

上記のArduinoスケッチのほかに、PC側のPowerShellツール(同期、EEPROMログ回収、EEPROMクリア、波形データ回収)を用意しています。

読みだした生データ(EEPROM)

02E7085F690004AA0300000009004B42
01000000000000000308000000000000
0286325F690000380100000000000000
02C3395F690000400337000000000313
034600000000123F03030C000100189E
03000306000018AD03030602020018BE
03040702030018BFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

読みだしたデータ(波形分)(太文字は追記コメント)

1621795     ←1件目のイベント検出 millis
1621745 ←2件目のイベント検出 millis
0,1,0,0 ←ここから1件目の波形データ
0,0,0,0
0,0,1,3
0,1,0,0
4,4,0,0
1,7,2,0 ←ここで閾値越え
0,2,2,0
0,0,2,1
0,0,1,2
1,0,0,1
3,0,0,0
2,1,0,0
2,3,0,0
1,4,1,0
0,4,0,0
0,1,1,0
0,0,0,0
0,0,0,1
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0 ←ここのへんから2件目の波形データ
0,3,0,0
0,0,1,1
0,0,0,1
3,0,0,0
2,6,0,0 ←ここで閾値越え
0,4,1,0
0,2,2,1
0,0,1,1
0,0,1,2
1,0,0,1
1,0,0,1
3,1,0,0
2,2,0,0
1,2,0,0
0,3,0,0
0,1,0,0
0,1,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0

本番運用開始直前の検証結果

[ EEPROM Data Analysis Report ]
Type | Date Time (UTC/Local) | Millis (raw) | Details
--------------------------------------------------------------------------------
SYNC | 2026-01-08 10:31:19 | 305664 | Time Synchronized (Unix:1767835879)
DATA | 2026-01-08 11:48:25 | 4932096 | CH0:0, CH1:0, CH2:0, CH3:9
RSET | 2026-01-08 13:28:39 | 0 | System Boot / Reset Detected
DATA | 2026-01-08 13:28:39 | 0 | CH0:8, CH1:0, CH2:0, CH3:0
SYNC | 2026-01-08 13:28:54 | 14336 | Time Synchronized (Unix:1767846534)
RSET | 2026-01-08 13:59:30 | 0 | System Boot / Reset Detected
SYNC | 2026-01-08 13:59:47 | 16384 | Time Synchronized (Unix:1767848387)
DATA | 2026-01-08 14:02:52 | 201472 | CH0:55, CH1:0, CH2:0, CH3:0
DATA | 2026-01-08 14:19:26 | 1195776 | CH0:70, CH1:0, CH2:0, CH3:0
DATA | 2026-01-08 14:26:23 | 1613312 | CH0:3, CH1:12, CH2:0, CH3:1
DATA | 2026-01-08 14:26:27 | 1617152 | CH0:0, CH1:3, CH2:6, CH3:0
DATA | 2026-01-08 14:26:32 | 1621504 | CH0:3, CH1:6, CH2:2, CH3:2
DATA | 2026-01-08 14:26:32 | 1621760 | CH0:4, CH1:7, CH2:2, CH3:3
--------------------------------------------------------------------------------
Analysis Finished.

イベント情報は、上のログ解析結果のように確認できるようになりました。
14:00以降は、センサーを叩いて信号を検知できるか検証しました。
センサーの固定方法(ピエゾ素子の振動を貼り付けたボンドが押さえている?)の問題か、実験室より感度が鈍くなったような気がします。本番運用と変更して、残りのセンサーを使って感度を上げられないかを別途検討してみます。

当初の計画どおりとまでは言えませんが、これで、本番運用を開始できました。


関連記事

AE測定、長期間ロギングシステム構築 (その6) 最終検証編

前回までで、センサーから信号を取り出し、Arduinoで複数チャンネルのAD変換してイベントをとらえる事前検討が完了しました。ここでは、長期ロギングに向けて最終的な調整をしていきます。チャンネルごとにセンサーの設置状況も違うので、センサーごとに異なる閾値を設定したほうが良いと推測されます。
 

閾値検討用元データの取得用 Arduinoスケッチ

const int NUM_CHANS = 4;
const int MAX_HIST_INDEX = 200;
unsigned int histogram[MAX_HIST_INDEX + 1][NUM_CHANS];

// タイムアウト設定 (2時間 = 7,200,000ms)
const unsigned long TIMEOUT_MS = 7200000;
unsigned long startTime;

void setup() {
Serial.begin(115200);
ADCSRA &= ~(bit(ADPS2) | bit(ADPS1) | bit(ADPS0));
ADCSRA |= bit(ADPS2);

for (int i = 0; i <= MAX_HIST_INDEX; i++) {
for (int j = 0; j < NUM_CHANS; j++) histogram[i][j] = 0;
}

Serial.println(F("Long-term Noise Analysis Started..."));
Serial.println(F("Target: 20,000 non-zero samples OR 2 hours."));
startTime = millis();
}

void loop() {
bool limitReached = false;
unsigned long nonZeroCount = 0; // 0以外の総サンプル数

while (!limitReached) {
// タイムアウトチェック
if (millis() - startTime >= TIMEOUT_MS) {
Serial.println(F("Time limit reached (2 hours)."));
limitReached = true;
break;
}

for (int ch = 0; ch < NUM_CHANS; ch++) {
int rawVal = analogRead(A0 + ch) >> 2;

// 0以外の場合のみカウント
if (rawVal > 0) {
int index = (rawVal > MAX_HIST_INDEX) ? MAX_HIST_INDEX : rawVal;
histogram[index][ch]++;
nonZeroCount++;

// 0以外のサンプルが合計20,000に達したら終了
if (nonZeroCount >= 20000) {
Serial.println(F("Non-zero sample limit reached."));
limitReached = true;
break;
}
}
}
}

// 結果の出力
Serial.println(F("Value,CH0,CH1,CH2,CH3"));
for (int i = 1; i <= MAX_HIST_INDEX; i++) { // 0は除外して出力
Serial.print(i);
if (i == MAX_HIST_INDEX) Serial.print("+");
for (int ch = 0; ch < NUM_CHANS; ch++) {
Serial.print(",");
Serial.print(histogram[i][ch]);
}
Serial.println();
}

Serial.print(F("Total Non-Zero Samples: ")); Serial.println(nonZeroCount);
Serial.print(F("Elapsed Time (ms): ")); Serial.println(millis() - startTime);
Serial.println(F("--- Analysis Finished ---"));
while (1);
}

開発環境(室内)での事前検証結果

Long-term Noise Analysis Started...
Target: 20,000 non-zero samples OR 2 hours.
Time limit reached (2 hours).
Value,CH0,CH1,CH2,CH3
1,0,2,2,1
2,0,2,0,0
3,0,0,0,1
4,0,0,0,0
5,0,0,0,0
6,0,0,0,0
7,0,0,0,0
8,0,0,0,0
9,0,0,0,0
10,0,0,0,0
11,0,0,0,0
12,0,0,0,0
13,0,0,0,0


186,0,0,0,0
187,0,0,0,0
188,0,0,0,0
189,0,0,0,0
190,0,0,0,0
191,0,0,0,0
192,0,0,0,0
193,0,0,0,0
194,0,0,0,0
195,0,0,0,0
196,0,0,0,0
197,0,0,0,0
198,0,0,0,0
199,0,0,0,0
200+,0,0,0,0
Total Non-Zero Samples: 8
Elapsed Time (ms): 7200214
--- Analysis Finished ---

この結果から、2時間放置してもノイズ信号のMax値は3であり、少しだけ余裕を見て閾値を5に設定すればよさそうだと判断しました。 そして次はフィールドテストへシフトしました。

フィールドテストでの追加検討

 機器類を整理して、実環境に配置して、閾値を再検討してみました。2時間の放置検証して、事前検証と同じ結果を得ました。 チャンネル別に閾値を設定する想定でしたが、かなり低い閾値設定なので、すべてのチャンネルとも5に設定することにします。

 これで、長期ロギングに向けた最終検討に移行できそうです。
次回は、最終検証、本番運用に移行していきます。


関連記事

AE測定、長期間ロギングシステム構築 (その5) 複数信号キャプチャ実験編

前回までで、センサーからの信号をArduinoでAD変換して簡易オシロで観測するところの事前検討が完了しました。最終的なシステム構成の確定に向けた検証をしていきます。
 ここでは、前回課題として見つけたチャンネル間クロストークの影響の見極めと使用チャンネル数の確定をします。2ch分の取り出した(AE)信号をAD変換して読み込む実験を行い、妥当な情報をロギングできそうかを検証していきます。
 前回の確認で4チャンネルでもそれなりの波形確認できそうなので、2回路分の実装と2回路分のダミー実装(半固定抵抗を通してGND接続)の合計4回路分を用意しました。

分圧回路
分圧回路

作成済みの2回路分を接続しました。

2ch
2ch

これに2ch分のAE(ピエゾ素子)をつないで、信号を検証しました。Arduinoスケッチは前回のものと同じです。

2chのAEセンサーを横並びに机に押し当てて、その左側、右側の机をペンで叩いて衝撃を計測してみました。

2ch


 A0チャンネルとA1チャンネルの信号の大きさが交互に変わっていることがわかります。また、 A2、A3チャンネルは 0のままで、影響を受けていないことが分かります。現状のチャンネル切り替えスピードで影響を受けずに計測できそうです。

4チャンネルでも行けそうなので、回路構成は4chで確定して進めることにしました。
つぎは、最終的な本番構成にして、動作確認して問題なければフィールドテストでスライスレベルの調整などを検討しましょう。

関連記事