| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- # -*- coding: utf-8 -*-
- """B翼 温度NBM (windscada M6a, 2026-08-24). 判据出处: skill temp-nbm + 如东教训七坑设防.
- 方法: 发电态×功率档(500kW) 同工况 fleet 相对偏差 (ref=2025-H2 基线代 / cur=末90天), n加权.
- 七坑: ①恒值哨兵(契约层已排GFilB2/B3) ②传感器不可信 dev≥15K→A类非热 ③返服run-in(cur运行量不足→不可判)
- ④配对差(主轴承双温/发电机NDE-DE) ⑤热汇列(环温/舱温自身偏→该台ΔT类解释存疑, 单列注记)
- ⑥冷浸盲区(发电态过滤天然规避) ⑦相对判据配绝对量(dev以K计, 报警需绝对dev≥4K非只z)."""
- import numpy as np, pandas as pd, pathlib
- from src.windscada.config import farm
- from app_ETL.app_ETL_guanlan.api import load_10min, contracted_cols
- from ..perf.curtail import classify
- PBINS = np.arange(0, 4500, 500)
- SENSOR_IMPLAUSIBLE_K = 15.0
- # 季节解耦闸: 环温通道自身窗间位移超此值 ⇒ 两窗季节错配, common_shift 拒绝出结论 (见其 docstring)
- SEASON_MATCH_MAX_K = 1.0
- PAIRS = {'主轴承双温差': ('tmp_wtc_MBearHTm_mean', 'tmp_wtc_MBearGTm_mean'),
- '发电机轴承NDE-DE(G-R)': ('tmp_wtc_GenBeGTm_mean', 'tmp_wtc_GenBeRTm_mean')} # 官方IO表: G=NDE, R=DE (2026-08-26 定案)
- HEATSINK = ['tmp_wtc_AmbieTmp_mean', 'tmp_wtc_NacelTmp_mean']
- # 跨线联动跟踪名单 (2026-08-26 与振动线约定, 机器位非口头):
- # 触发 = 最新月 dev≥2.5K 或 位次进前3 → temp_overview 置顶显示 [跨线通知], 见即发信箱给振动线
- WATCHLIST = {
- ('WTG17', 'tmp_wtc_GenBeRTm_mean'): '17# 发电机DE(R端): 振动iso_rms_vel排1/35台, 温度2026-02起爬升(07月+2.3K第7)',
- ('WTG23', 'tmp_wtc_GenBeGTm_mean'): '23# 发电机NDE(G端): 振动单端1.5-1.9×fleet, 温度全程正常(对照台)',
- # 29# 机制=润滑断供·泵本体 (振动线v5定案, 2026-08-27更正: 非新报警非轴承损伤).
- # 温度/振动尖峰/柱塞=同一机制的果侧两观测+因侧一观测, 禁写"三源印证"(振动线④纪律采纳).
- # 柱塞两段式签名(mblub_monthly, 全纵向口径): 25-04~10单次时长由自身25-01~03的132~261s塌到34~57s→温度峰(25-05/08第1);
- # 26-07窗(至07-07)**单月突变**: 26-06单次68s完全正常→07窗1784s=自身18月区间(34~261s)的7~52倍,
- # 总打脂10.3h/天 vs 自身正常~1.4h/天, counts 6天窗应有288实测145=50% = 泵打不满挣扎态;
- # 突变后数日振动尖峰起(07-11首发在台风窗内, 窗外净尖峰07-24起). [口径勘误2026-08-27: 原"6×中位/70×
- # 对照台"为横比, 柱塞单次时长跨台天然异质(p90/p10=16~100×)横比无效, 双线一致弃用, 判读只认自身纵向]
- # 并读纪律: 温度dev与振动尖峰频次同步恶化才升级, 单独一个上升可能是工况.
- # 修泵后验收正本 = 振动线 findings.json TCM-16 (三分支决策树带量化门: A尖峰消14天/20条/max<8扣台风,
- # B柱塞回带尖峰不消才内窥, C柱塞不回带重排泵/管路; 温度不降不否证A). 本处只留指针不复制, 防载体漂移.
- # ★温度滞后条款(TCM-16③): 泵修好后温度dev允许滞后1~2月缓降 — watch通知若在修后窗内触发, 先对照TCM-16再升级.
- ('WTG29', 'tmp_wtc_MBearGTm_mean'): '29# 后主轴承(齿轮箱侧): 润滑断供机制(v5), 温度=果侧观测, 与振动尖峰并读',
- ('WTG29', 'tmp_wtc_MBearHTm_mean'): '29# 前主轴承(轮毂侧): 同上, 双端并盯',
- }
- def watchlist_check(cfg=None):
- """联动名单当前状态: 最新月 dev 与位次; 触发返回 [跨线通知] 行."""
- cfg = cfg or farm()
- import pandas as _pd
- f = pathlib.Path(cfg['store']) / 'watch_channels_monthly.parquet'
- if not f.exists():
- return ['[联动跟踪] watch_channels_monthly 仓缺失 — 跟踪失效, 须重建 scripts/windscada_watch_channels_build.py (守护失败必响亮)']
- df = _pd.read_parquet(f)
- out = []
- for (t, ch), why in WATCHLIST.items():
- piv = df.pivot_table(index='month', columns='turbine', values=ch)
- m = piv.index.max()
- row = piv.loc[m]
- dev = float(row[t] - row.median())
- rank = int(row.rank(ascending=False)[t])
- n = int(row.notna().sum())
- hit = dev >= 2.5 or rank <= 3
- tag = '🔔[跨线通知-发信箱给振动线]' if hit else '[联动跟踪中]'
- out.append(f"{tag} {t} {ch.replace('tmp_wtc_','').replace('_mean','')} {m}: dev{dev:+.1f}K 位次{rank}/{n} (门=dev≥2.5K∨前3) | {why}")
- return out
- def build_store(cfg=None, span=None, write=True):
- """温度 NBM 比档件。`span=(起, 止)` 给了就把 `cur` 档换成**所选时间窗**(含两端,用户令 2026-09-21);
- `ref`/`ref_sm` 两个对照窗保持不变;span 模式走 `slim10min` 窄仓且**不写盘**(不覆盖正式产物)。
- """
- cfg = cfg or farm()
- tmps = [x for x in contracted_cols(cfg, groups=['B.温度NBM']) if x.endswith('_mean')]
- rows, failed = [], []
- for t in cfg['turbines']:
- try:
- if span:
- from app_ETL.app_ETL_guanlan.api import slim
- # ★一次读、内存里切三窗(2026-09-21 性能实测): 三窗各读一次 = 3×38 次 parquet 读,
- # 每台 261k 行 ⇒ 37 s/窗。改成"按列裁剪读一次 + 内存掩码切片" ⇒ 列少一半、读一次,
- # 口径不变(掩码仍按各窗边界,逐行同源)。
- _cols_t = ['ts', 'grd_wtc_ActPower_mean', 'tur_wtc_PowerRef_endvalue',
- 'tur_wtc_SecAnemo_mean', 'tur_wtc_GenRpm_mean',
- *[c for c in tmps if c not in ('grd_wtc_ActPower_mean',)]]
- d = slim.load(t, cfg, span=None, columns=_cols_t)
- else:
- d = load_10min(t, cfg, groups=['A.功率', 'A.风况', 'A.转速', 'B.温度NBM'])
- except Exception as e:
- # 守护失败必响亮: 原为裸 `except: continue`, 数据根整个搬走时会**静默**产出空仓
- # (2026-09-07 实测: 源盘路径失效 → 38 台全失败 → 0 行落盘, 无任何报错, 只能手工 debug)。
- failed.append((t, f'{type(e).__name__}: {e}'))
- continue
- d['state'] = classify(d, cfg['rated_kw'])
- d = d[(d['state'].isin(['正常发电', '限电·绑定', '限电命令·未绑定'])) & (d['grd_wtc_ActPower_mean'] > 200)]
- cur_lo = str(d.ts.max() - pd.Timedelta(days=90))[:10]
- # ★ref_sm (2026-09-07): 与 cur **同历月、早一年**的季节匹配窗, 供 common_shift 做季节解耦。
- # 为什么不沿用互斥 np.where: ref_sm 与 ref 有约一周重叠, 互斥赋值会**静默削掉** ref_sm 一截
- # (先匹配者胜)。改为每窗独立取子集 → 允许重叠, 且 **ref/cur 的内容逐行不变** (加窗零副作用)。
- # span 模式: ref_sm 跟着**所选窗**早一年(季节匹配的是用户选的窗,不是"数据末端 90 天")。
- if span:
- sm_lo = str((pd.Timestamp(span[0]) - pd.DateOffset(years=1)).date())
- sm_hi = str((pd.Timestamp(span[1]) - pd.DateOffset(years=1)).date())
- else:
- sm_lo = str(pd.Timestamp(cur_lo) - pd.DateOffset(years=1))[:10]
- sm_hi = str(d.ts.max() - pd.DateOffset(years=1))[:10]
- d['pbin'] = pd.cut(d['grd_wtc_ActPower_mean'], PBINS).map(lambda iv: iv.mid if pd.notna(iv) else np.nan)
- d = d.dropna(subset=['pbin'])
- # 掩码必须在 dropna **之后**建: 先建会让布尔序列索引与 d 错位, pandas 靠索引静默重对齐
- # (UserWarning: Boolean Series key will be reindexed) —— 那是会出错的地方, 不留。
- # 窗边界仍取自 state 过滤后、pbin 过滤前的 tmax (与加窗前口径一致, 保 cur 逐行不变)。
- # ★2026-09-21 用户令「判级按所选时间窗」: span 给了就把 `cur` 档换成所选窗(含两端),
- # `ref`/`ref_sm` 仍固定(季节解耦的对照窗必须钉住,否则失去对照意义)。
- if span:
- _a, _b = pd.Timestamp(span[0]), pd.Timestamp(span[1]) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
- cur_m = (d.ts >= _a) & (d.ts <= _b)
- else:
- cur_m = d.ts >= cur_lo
- wins = {'ref': (d.ts >= '2025-07-01') & (d.ts < '2026-01-01'),
- 'cur': cur_m,
- 'ref_sm': (d.ts >= sm_lo) & (d.ts <= sm_hi)}
- for wname, m in wins.items():
- dw = d[m]
- if dw.empty: # 早期场可能没有早一年的数据 → 该窗缺, common_shift 会 raise 而非静默
- continue
- if span:
- # ★按窗重算走**批量**分组(2026-09-21 性能实测: 逐通道 27 次 groupby/台 ⇒ 37 s/窗;
- # 一次 groupby 全通道 ⇒ ~2 s。口径逐格相同: 同 median、同 size 门 ≥18,
- # 只是行序不同 —— 默认路径保持原样不动,正式产物 byte 级不变。
- cols = [c for c in tmps if c in dw.columns]
- g = dw.groupby('pbin', observed=True)[cols].agg(['median', 'size'])
- for ch in cols:
- sub = g[ch]
- sub = sub[sub['size'] >= 18]
- for pb, r in sub.iterrows():
- rows.append(dict(turbine=t, channel=ch, win=wname, pbin=float(pb),
- med=float(r['median']), n=int(r['size'])))
- continue
- for ch in tmps:
- if ch not in dw.columns:
- continue
- g = dw.groupby('pbin', observed=True)[ch].agg(['median', 'size']).reset_index()
- g = g[g['size'] >= 18]
- for _, r in g.iterrows():
- rows.append(dict(turbine=t, channel=ch, win=wname, pbin=float(r['pbin']),
- med=float(r['median']), n=int(r['size'])))
- if write:
- print(t, flush=True)
- if failed:
- print(f'⚠ {len(failed)}/{len(cfg["turbines"])} 台加载失败: '
- + '; '.join(f'{t} {e}' for t, e in failed[:3])
- + (' …' if len(failed) > 3 else ''), flush=True)
- if not rows:
- raise RuntimeError(
- f'build_store 一行都没产出 ({len(failed)}/{len(cfg["turbines"])} 台加载失败) — '
- f'源目录 {cfg.get("src_10min")} 是否存在? 不静默落空仓覆盖既有 store。'
- + (f' 首个错误: {failed[0][1]}' if failed else ''))
- st = pathlib.Path(cfg['store']); st.mkdir(parents=True, exist_ok=True)
- out = pd.DataFrame(rows)
- if write:
- out.to_parquet(st / 'temp_bins.parquet')
- print('→ temp_bins.parquet', len(rows), f'(窗: {sorted({r["win"] for r in rows})})')
- return out
- def registry(cfg=None, win='cur', span=None):
- """温度 NBM 判级。`span=(起, 止)` 给了就**按所选时间窗**重算 `cur` 档(用户令 2026-09-21):
- `ref`(2025-07~2025-12 干净窗)与 `ref_sm`(同历月早一年)仍是**固定基线**——
- 它们的作用是"季节解耦的对照窗",跟着用户窗漂移就失去对照意义;页面据此如实标注。
- """
- cfg = cfg or farm()
- if span:
- tb = build_store(cfg, span=span, write=False)
- else:
- tb = pd.read_parquet(pathlib.Path(cfg['store']) / 'temp_bins.parquet')
- tb = tb[tb.win == win]
- # ★2026-09-21 实逮(服务器按窗重算时): 窗内样本退化(如"近30日"=2026-09 只有 1 个节拍 ⇒ 比档件为空)时,
- # `groupby(...).apply(...)` 返回的是**空 DataFrame** 而不是 Series ⇒ `.rename('dev_K')` 抛
- # `TypeError: Index(...) must be called with a collection of some kind, 'dev_K' was passed`,
- # 把整面判级打断。退化窗应当**如实"不可判"**(样本不足),不是崩。
- if not len(tb):
- return pd.DataFrame(columns=['turbine', 'channel', 'dev_K', '判'])
- fleet = tb.groupby(['channel', 'pbin'])['med'].median().rename('fleet_med')
- tb = tb.join(fleet, on=['channel', 'pbin'])
- tb['dev'] = tb['med'] - tb['fleet_med']
- dev = tb.groupby(['turbine', 'channel']).apply(lambda x: float(np.average(x['dev'], weights=x['n'])), include_groups=False).rename('dev_K').reset_index()
- mad = dev.groupby('channel')['dev_K'].apply(lambda s: float(1.4826 * np.median(np.abs(s - s.median())))).rename('mad')
- dev = dev.join(mad, on='channel')
- dev['z'] = dev['dev_K'] / dev['mad'].clip(lower=0.3)
- nrows = tb.groupby('turbine')['n'].sum().rename('n_rows')
- dev = dev.join(nrows, on='turbine')
- def verdict(r):
- if r['n_rows'] < 500:
- return '不可判(样本不足/返服嫌疑)'
- if r['dev_K'] >= SENSOR_IMPLAUSIBLE_K:
- return '传感器候选(A类,非热)'
- if r['dev_K'] >= 4.0 and r['z'] >= 3.0:
- return '温度候选(定向查)'
- if r['dev_K'] >= 2.5 and r['z'] >= 2.0:
- return '记基线观察'
- return '—'
- dev['判'] = dev.apply(verdict, axis=1)
- dev['热汇注'] = dev['channel'].isin(HEATSINK)
- dev = dev.sort_values('dev_K', ascending=False)
- # ★批次盲声明 (实验 K): dev_K/z 是 fleet 相对量, 对全场同步平移逐值不变 ⇒ 本表**看不见批次退化**。
- # 判'—'只等于"该台不偏离机群", 不等于"该台正常"。共模轴须看 common_shift()。
- dev.attrs['batch_blind'] = {
- 'blind_to': '全场同步平移 (批次退化 / 场级参考量变化)',
- 'why': "dev_K = med − fleet_med; 全体同移则 dev_K 逐值不变 (实测 max|Δz|=0.00e+00)",
- 'not_a_threshold_issue': True,
- 'read_as': "判'—' ≠ 该台正常, 只等于'不偏离机群'; 共模轴见 temp_nbm.common_shift()",
- }
- return dev
- def common_shift(cfg=None, win_cur='cur', win_ref=None):
- """共模图 — registry 缺的那另一半 (实验 K 接线 2026-09-07; 判据族见 sop.discriminators.fleet_two_component_split)。
- registry() 算 `fleet_med` 只为把它**减掉**, 自身从不被看。但 `d − fleet_med` 对全场同步平移
- **逐值恒等不变** (如东 38 台×4 通道实测 max|Δz|=0.00e+00) ⇒ **批次退化在 registry 上必然全绿**,
- 且这不是阈值问题, 调阈值无效。本函数补上被丢掉的那一半: 同工况分档下 fleet_med 自身 cur vs ref 的位移。
- 数据不用新采 —— `temp_bins.parquet` 里 ref(2025-07~2026-01) 与 cur(末90天) 两窗都在, registry 只读了 cur。
- ⚠⚠ **季节混杂 — 本函数在如东当前 store 上给不出结论 (2026-09-07 首跑实测, 非理论顾虑)**:
- build_store 的 ref(2025-07~2026-01) 与 cur(末90天) **不同历月**, pbin 只匹配**载荷**没匹配**环温**。
- 实测 `amb_common_K = −5.5K` ⇒ 两窗差大半个季节。此时"raw 减环温"的一阶修正 (隐含各通道对环温增益≈1)
- 把**全部 19 个通道同向抬成候选 = 19/19 全报 = 零分辨力** —— 真实增益逐通道不同 (热汇类≈1, 强自发热部件<1)。
- 故本函数设**季节解耦闸**: |amb_common_K| ≥ SEASON_MATCH_MAX_K 时全表判 INSUFFICIENT, **拒绝出结论**,
- 不发全红表让人自己悟。要真答案走 `sop.discriminators.fleet_two_component_split`
- (逐台 OLS 扣 P/环温 + 季节匹配 H0 + 时序 SPRT) 或 `nbm_residual(common_*_window=...)`。
- ⇒ 本表当前的用途 = **让批次盲这件事在 UI 上可见**, 外加 `apparent_gain` 诊断列; **不是**批次退化检出器。
- 修法 (待裁, 需改 build_store 并重建 store): 加一个与 cur **同历月、早一年**的窗 (如 ref_sm), 两窗季节对齐。
- ⚠ 共模图**不能单独定因**: 全场同移既可能是批次退化, 也可能是场级参考量变化 (环温族偏/微气象/控制版本
- 全场升级/限电制度切换) ⇒ 单靠本表最高 **准定论·预警**, 升定论须外部绝对锚。
- 返回 DataFrame: channel / common_K_raw / common_K_net / amb_common_K / vs_mad(net 相对台间散布 MAD 的倍数)
- / n_pbin / 判 / 热汇注。
- """
- cfg = cfg or farm()
- tb = pd.read_parquet(pathlib.Path(cfg['store']) / 'temp_bins.parquet')
- if win_ref is None: # 优先季节匹配窗; 老 store 无 ref_sm 时退回 ref, 由季节解耦闸兜底判 INSUFFICIENT
- win_ref = 'ref_sm' if 'ref_sm' in set(tb['win'].unique()) else 'ref'
- if not {win_cur, win_ref} <= set(tb['win'].unique()):
- raise KeyError(f'temp_bins 缺窗 {win_cur}/{win_ref} (实有 {sorted(tb["win"].unique())}); '
- '共模图是硬前置, 不静默跳过')
- # 每 (win, channel, pbin) 的 fleet 中位 = registry 里被减掉的那个量
- fm = tb.groupby(['win', 'channel', 'pbin']).agg(fleet_med=('med', 'median'), n=('n', 'sum')).reset_index()
- cur = fm[fm.win == win_cur].set_index(['channel', 'pbin'])
- ref = fm[fm.win == win_ref].set_index(['channel', 'pbin'])
- j = cur.join(ref, how='inner', lsuffix='_c', rsuffix='_r')
- j['shift'] = j['fleet_med_c'] - j['fleet_med_r']
- j['w'] = j[['n_c', 'n_r']].min(axis=1)
- raw = j.groupby('channel').apply(
- lambda x: float(np.average(x['shift'], weights=x['w'])), include_groups=False).rename('common_K_raw')
- npb = j.groupby('channel').size().rename('n_pbin')
- out = pd.concat([raw, npb], axis=1).reset_index()
- # 一阶季节修正: 减环温通道自身的共模位移 (假定增益≈1 — 粗, 见 docstring)
- amb_row = out[out.channel == 'tmp_wtc_AmbieTmp_mean']['common_K_raw']
- amb_k = float(amb_row.iloc[0]) if len(amb_row) else np.nan
- out['amb_common_K'] = amb_k
- out['common_K_net'] = out['common_K_raw'] - (amb_k if np.isfinite(amb_k) else 0.0)
- # 相对台间散布: 全场整体移了多少个"台间 MAD" — >1 即整体位移已超过 registry 能看见的散布
- dev = registry(cfg, win=win_cur)
- mad = dev.groupby('channel')['dev_K'].apply(
- lambda x: float(1.4826 * np.median(np.abs(x - x.median())))).rename('mad')
- out = out.join(mad, on='channel')
- out['vs_mad'] = out['common_K_net'] / out['mad'].clip(lower=0.3)
- # 表观增益 = 该通道随环温走了几成 (诊断量; 自发热强的部件 <1, 热汇类 ≈1)
- # ⚠ 只在环温**确有可观位移**时才算表观增益 —— 季节对齐后 amb_k→0, 除以近零分母会得出 7~8 的假增益
- # (2026-09-07 装 ref_sm 后实测: amb_k 从 −5.54K 降到 −0.31K, gain 立刻虚涨到 7.5~8.4)。
- # 本列只是"季节错配时"的诊断量, 季节对齐时它无意义 → NaN, 不给假数。
- out['apparent_gain'] = (out['common_K_raw'] / amb_k
- if (np.isfinite(amb_k) and abs(amb_k) >= SEASON_MATCH_MAX_K) else np.nan)
- # ★季节解耦闸 (2026-09-07 首跑实测逼出): 环温自身窗间位移 = 两窗季节错配的**实测**代理。
- # 如东实跑 amb_common_K = −5.5K ⇒ ref(7~12月) 与 cur(末90天) 差了大半个季节。此时
- # "raw 减环温" 的一阶修正 (假定增益≈1) 会把**全部 19 通道**同向抬成候选 = 19/19 全报 = 零分辨力。
- # 故此处**不许出结论**: 全表判 INSUFFICIENT 并指向真判据, 而不是发一张全红的表让人自己悟。
- # (失效必须落在显式状态 — 同 memory parse-failure-read-as-positive-result 的反向: 别让"全阳"冒充检出。)
- season_decoupled = bool(np.isfinite(amb_k) and abs(amb_k) < SEASON_MATCH_MAX_K)
- out.attrs['season_decoupled'] = season_decoupled
- out.attrs['amb_common_K'] = amb_k
- out.attrs['win_ref'] = win_ref # 用了哪个参考窗 — 别让读者猜 ref 还是 ref_sm
- def verdict(r):
- if not np.isfinite(amb_k):
- return '不可判(环温通道缺, 季节无法修)'
- if not season_decoupled:
- return f'INSUFFICIENT(季节未解耦: 环温窗间移{amb_k:+.1f}K)'
- if r['channel'] == 'tmp_wtc_AmbieTmp_mean':
- return '— (季节基准本身)'
- if r['n_pbin'] < 3:
- return '不可判(共同档位<3)'
- a = abs(r['common_K_net'])
- if a >= SENSOR_IMPLAUSIBLE_K:
- return '共模巨移(口径/数据嫌疑, 非热)'
- if a >= 2.5 and abs(r['vs_mad']) >= 1.0:
- return '共模漂移候选(批次/场级, 须外部锚)'
- if a >= 1.5:
- return '记基线观察'
- return '—'
- out['判'] = out.apply(verdict, axis=1)
- out['热汇注'] = out['channel'].isin(HEATSINK)
- return out.sort_values('common_K_net', key=lambda s: s.abs(), ascending=False).reset_index(drop=True)
- def pairs_table(cfg=None):
- cfg = cfg or farm()
- tb = pd.read_parquet(pathlib.Path(cfg['store']) / 'temp_bins.parquet')
- tb = tb[tb.win == 'cur']
- out = []
- for name, (a, b) in PAIRS.items():
- pa = tb[tb.channel == a].groupby('turbine')['med'].median()
- pb = tb[tb.channel == b].groupby('turbine')['med'].median()
- dd = (pa - pb).dropna().rename('pair_diff').reset_index()
- dd['pair'] = name
- med = dd['pair_diff'].median()
- dd['pair_dev'] = dd['pair_diff'] - med
- out.append(dd)
- return pd.concat(out, ignore_index=True)
|