"""
### 目的
"""
"""
今回は打音検査で得られた音声データを基に可視化とモデル構築を作ります。
"""
"""
### **打音検査とは**
"""
"""
トンネルなどの表層のコンクリート強度を調べるために打音検査が行われています。<br>
一般には検査員がハンマーでコンクリート表面をたたき、清音（キンキンという音）か濁音（ボコボコという音）で良し悪しを判定します。<br>
ウェーブレット変換によるオクターブ分析で、おおまかな周波数分布や、各周波数での減衰状況、揺らぎのある無しで判定するのが有効だと思えます。<br>
> [http://ensoftware.net/analysis/daon.html](http://)<<br> 

"""
"""
このように減衰状況などの特徴を抽出することで精度が上がるのではないかと考えた上で今回はlocaloutfilterを用いた教師なし異常検知を用いてみようと思います<br>
参考にしたnotebook<br>
https://www.kaggle.com/nejumi/simple-unsupervised-baseline<br>
https://www.kaggle.com/t88take/hah-exp005-simple-baseline<br>
"""
#ライブラリのインポート(pywtはウェーブレット解析用です)
import numpy as np
import pandas as pd
import librosa 
import librosa.display
import matplotlib.pyplot as plt
import IPython.display as ipd
import os
import pywt
from tqdm.notebook import tqdm
from tqdm._tqdm_notebook import tqdm_notebook
tqdm_notebook.pandas(desc="Processing:")
import soundfile as sf
from sklearn.metrics import roc_auc_score
from sklearn.neighbors import LocalOutlierFactor
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import MinMaxScaler
from sklearn.svm import OneClassSVM
from sklearn.model_selection import GridSearchCV, train_test_split
import scipy.stats
#データパスの指定をします
INPUT_DIR = '/kaggle/input/hah-data-science-challenge'
def meta_define():
    """
    input : none
    output : Corrected metadata
    """
    import pandas as pd
    import os 
    os.chdir("/kaggle/input/hah-data-science-challenge/")
    df_train = pd.read_csv("train.csv", index_col=False)
    df_test = pd.read_csv("test.csv", index_col=False)
    
    ##################################################
    #以下辞書や変数の定義
    #各データ修正用の辞書
    bolt_dict = {
        '大':"big",
        '小':"small"
    }

    plate_dict = {
        '大':"big",
        '小':"small"
    }

    record_dict = {
        'PC内臓':"pc_built_in",
        'PC内蔵':"pc_built_in",
        'USB1':"usb1", 
        'USB2':"usb2", 
        'USB3':"usb3", 
        'USB4':"usb4", 
        'スマホ':"smart_phone",
        'スマホのボイスレコーダ':"smart_phone",
        '内蔵マイク':"pc_built_in",
        }

    distance_dict = {
        '10cm': 0.1, 
        '10㎝': 0.1, 
        '1M': 1.0, 
        '20cm': 0.2, 
        '20㎝': 0.2, 
        '2M': 2.0, 
        '2m': 2.0, 
        '30cm': 0.3, 
        '30cn': 0.3, 
        '30㎝': 0.3, 
        '3m': 3.0, 
        '40cm': 0.4, 
        '40㎝': 0.4, 
        '50cm': 0.5, 
        '50㎝': 0.5, 
        '5cm': 0.05,
        '8cm': 0.08, 
        '１Ｍ': 1.0   
    }

    cvt_dict = {
        "ねじ" : bolt_dict, 
        'プレート' : plate_dict, 
        '録音方法' : record_dict, 
        'マイク距離' : distance_dict
    }
    
    #df_train日本語カラム名 : ['ID', 'ねじ', 'プレート', '録音方法', 'マイク距離', 'ファイル', 'Target']
    col_train = ['id', 'bolt', 'plate', 'record', 'mic_dist', 'file', 'target']
    #df_test日本語カラム名 : ['ID', 'ねじ', 'プレート', '録音方法', 'マイク距離', 'ファイル', 'Target']
    col_test = ['id', 'bolt', 'plate', 'record', 'mic_dist', 'file']
    
    tgt_col = ["ねじ", 'プレート', '録音方法', 'マイク距離']
    ##################################################
    
    for col in tgt_col:#Targetは変換対象外
        df_train[col] = df_train[col].map(cvt_dict[col])
        df_test[col] = df_test[col].map(cvt_dict[col])
        
    df_train.columns = col_train
    df_test.columns = col_test
    
    return df_train, df_test
#read data
df_train, df_test = meta_define()
#display data top 5
df_train.head()
#display data under 5
df_train[df_train["target"]==0.0]
#define function for displaying wave
def display_wav(file_name):
    wav, sr = librosa.load(file_name, sr=None)
    plt.figure()
    plt.figure(figsize=(15, 5))
    librosa.display.waveplot(wav, sr)
    plt.show()
#plot normal sound train2.wav
display_wav("train/train/train2.wav")
ipd.Audio("train/train/train2.wav")
display_wav("train/train/train31.wav")
ipd.Audio("train/train/train31.wav")
display_wav("train/train/train46.wav")
ipd.Audio("train/train/train46.wav")
df_train[df_train["target"]==1.0]
display_wav("train/train/train482.wav")
ipd.Audio("train/train/train482.wav")
display_wav("train/train/train785.wav")
ipd.Audio("train/train/train785.wav")
"""
<span style="font-size:200%;"> **音の可視化（異常あり）**</span><br>
trainデータから異常なしのデータを見ようと思います。
"""
wav, sr = librosa.load("train/train/train785.wav", sr=None)
wav
widths = np.arange(1, 31)
cwtmatr, freqs = pywt.cwt(wav, widths, 'cmor1.5-1.0')
abs_result = np.abs(cwtmatr)
power_spec = librosa.amplitude_to_db(abs_result, ref=np.max)

plt.figure(figsize=(25,5))
librosa.display.specshow(power_spec, y_axis='log', x_axis='time', sr = sr)
plt.title('Power Spectrogram')
plt.colorbar(format='%+2.0f dB')

plt.tight_layout()
plt.show()
bins = 84
hop_length = 512
fmin = 32.7
cqt = librosa.cqt(wav,sr, hop_length=hop_length, fmin=fmin, n_bins=bins)
D = librosa.amplitude_to_db(cqt, ref=np.max) #定Q変換
frq = librosa.cqt_frequencies(n_bins=bins,fmin=fmin) #周波数軸
t = librosa.frames_to_time(np.arange(D.shape[1] + 1), sr, hop_length=hop_length) #時間軸
plt.pcolormesh(t, frq, 10 * np.log(np.abs(cqt)), cmap='jet')
plt.colorbar()
plt.show()
plt.close()
stft_result = librosa.stft(wav)
abs_result = np.abs(stft_result)
power_spec = librosa.amplitude_to_db(abs_result, ref=np.max)

plt.figure(figsize=(25,5))
librosa.display.specshow(power_spec, y_axis='log', x_axis='time', sr = sr)
plt.title('Power Spectrogram')
plt.colorbar(format='%+2.0f dB')

plt.tight_layout()
plt.show()
wav, sr = librosa.load("train/train/train482.wav", sr=None)
wav = (wav-wav.min())/(wav.max()-wav.min())
wav
widths = np.arange(1, 31)
cwtmatr, freqs = pywt.cwt(wav, widths, 'cmor1.5-1.0')
abs_result = np.abs(cwtmatr)
power_spec = librosa.amplitude_to_db(abs_result, ref=np.max)

plt.figure(figsize=(25,5))
librosa.display.specshow(power_spec, y_axis='log', x_axis='time', sr = sr)
plt.title('Power Spectrogram')
plt.colorbar(format='%+2.0f dB')

plt.tight_layout()
plt.show()
bins = 84
hop_length = 512
fmin = 32.7
cqt = librosa.cqt(wav,sr, hop_length=hop_length, fmin=fmin, n_bins=bins)
D = librosa.amplitude_to_db(cqt, ref=np.max) #定Q変換
frq = librosa.cqt_frequencies(n_bins=bins,fmin=fmin) #周波数軸
t = librosa.frames_to_time(np.arange(D.shape[1] + 1), sr, hop_length=hop_length) #時間軸
plt.pcolormesh(t, frq, 10 * np.log(np.abs(cqt)), cmap='jet')
plt.colorbar()
plt.show()
plt.close()
stft_result = librosa.stft(wav)
abs_result = np.abs(stft_result)
power_spec = librosa.amplitude_to_db(abs_result, ref=np.max)

plt.figure(figsize=(25,5))
librosa.display.specshow(power_spec, y_axis='log', x_axis='time', sr = sr)
plt.title('Power Spectrogram')
plt.colorbar(format='%+2.0f dB')

plt.tight_layout()
plt.show()
"""
<span style="font-size:200%;"> **音の可視化(異常なし)**</span><br>
"""
wav, sr = librosa.load("train/train/train2.wav", sr=None)
wav = (wav-wav.min())/(wav.max()-wav.min())
wav
widths = np.arange(1, 31)
cwtmatr, freqs = pywt.cwt(wav, widths, 'cmor1.5-1.0')
abs_result = np.abs(cwtmatr)
power_spec = librosa.amplitude_to_db(abs_result, ref=np.max)

plt.figure(figsize=(25,5))
librosa.display.specshow(power_spec, y_axis='log', x_axis='time', sr = sr)
plt.title('Power Spectrogram')
plt.colorbar(format='%+2.0f dB')

plt.tight_layout()
plt.show()
bins = 84
hop_length = 512
fmin = 32.7
cqt = librosa.cqt(wav,sr, hop_length=hop_length, fmin=fmin, n_bins=bins)
D = librosa.amplitude_to_db(cqt, ref=np.max) #定Q変換
frq = librosa.cqt_frequencies(n_bins=bins,fmin=fmin) #周波数軸
t = librosa.frames_to_time(np.arange(D.shape[1] + 1), sr, hop_length=hop_length) #時間軸
plt.pcolormesh(t, frq, 10 * np.log(np.abs(cqt)), cmap='jet')
plt.colorbar()
plt.show()
plt.close()
wav, sr = librosa.load("train/train/train31.wav", sr=None)
wav = (wav-wav.min())/(wav.max()-wav.min())
wav
widths = np.arange(1, 31)
cwtmatr, freqs = pywt.cwt(wav, widths, 'cmor1.5-1.0')
abs_result = np.abs(cwtmatr)
power_spec = librosa.amplitude_to_db(abs_result, ref=np.max)

plt.figure(figsize=(25,5))
librosa.display.specshow(power_spec, y_axis='log', x_axis='time', sr = sr)
plt.title('Power Spectrogram')
plt.colorbar(format='%+2.0f dB')

plt.tight_layout()
plt.show()
bins = 84
hop_length = 512
fmin = 32.7
cqt = librosa.cqt(wav,sr, hop_length=hop_length, fmin=fmin, n_bins=bins)
D = librosa.amplitude_to_db(cqt, ref=np.max) #定Q変換
frq = librosa.cqt_frequencies(n_bins=bins,fmin=fmin) #周波数軸
t = librosa.frames_to_time(np.arange(D.shape[1] + 1), sr, hop_length=hop_length) #時間軸
plt.pcolormesh(t, frq, 10 * np.log(np.abs(cqt)), cmap='jet')
plt.colorbar()
plt.show()
plt.close()
"""
### **モデル構築・学習・評価**
"""
# meta_dataにファイルパスを追加
df_train['filepath'] = INPUT_DIR + '/train/train/' + df_train['file']
df_test['filepath'] = INPUT_DIR + '/test/test/' + df_test['file']

# trainとtestをひとつに
df = pd.concat([df_train, df_test]).reset_index(drop=True)
# 一打音1レコードのメタデータを作成する
results = []
filelist = df['filepath'].to_list()

for i in tqdm(range(len(df))):
    filepath = df.loc[i, 'filepath']
    y, sr = librosa.load(filepath)
    onset_envelope = librosa.onset.onset_strength(y, sr)
    onset_times = librosa.onset.onset_detect(y=y, sr=sr, onset_envelope=onset_envelope, units='time')
    
    for j, onset_time in enumerate(onset_times):
        tmp = pd.DataFrame([df.loc[i]])
        tmp['split_id'] = j
        tmp['onset_time'] = onset_time
        tmp['play_time'] = y.size / sr
        results.append(tmp)
df_split = pd.concat(results)
# onset_detectで検出された時間を元に切り出す時間を設定
# onset_detectは、始点が若干遅れているのでマイナスのオフセットを付与
# オフセット掛けた始点から固定時間分を足した値を終点とする

st_offset = 0.05 # 始点のオフセット
duration = 0.2 # 切り出す時間
df_split['st_time'] = df_split['onset_time'] - st_offset
df_split['ed_time'] = df_split['st_time'] + duration
df_split['duration'] = duration

# 終点で固定時間に満たない場合は除外
df_split = df_split[df_split['ed_time'] <= df_split['play_time']].reset_index(drop=True)
# 切り出した打音ごとのsignalの最大値をメタデータに追加する
for i in tqdm(range(len(df_split))):
    filepath = df_split.loc[i, 'filepath']
    st_time = df_split.loc[i, 'st_time']
    duration = df_split.loc[i, 'duration']
    
    # loadするときにoffsetで始点、durationで期間を指定する
    y, sr = librosa.load(filepath, offset=st_time, duration=duration)
    df_split.loc[i, 'signal_max'] = max(abs(y))
df = df_split.copy()
# 音の大きさが一定以下のデータは無音として、対象から除外
th = 0.5
df = df[df['signal_max']>0.5].reset_index(drop=True)
df_labeled = df[df.target.isnull()==False]
# librosaで.wavファイルから特徴抽出します。
n_features = 84

features = np.zeros((len(df), n_features))
for i in tqdm(range(len(df))):
    filepath = df.loc[i, 'filepath']
    st_time = df.loc[i, 'st_time']
    duration = df.loc[i, 'duration']
    
    y, sr = librosa.load(filepath, offset=st_time, duration=duration)
    y = (y-y.min())/(y.max()-y.min()) # サンプルごとの正規化
    widths = np.arange(1, 85)
    cwtmatr, freqs = pywt.cwt(y, widths, 'cmor1.5-1.0')
    abs_result = np.abs(cwtmatr)
    power_spec = librosa.amplitude_to_db(abs_result, ref=np.max)
    ceps = power_spec.mean(axis=1)
    
    # 複数のローリングウィンドウでそれぞれ20次元のMFCCを得られるので、その平均をとる。
    features[i] = ceps
def outlier_scoring(train_data, pred_data, n_neighbors,leaf_size,metric):
    # 教師なし異常検知のアプローチで異常スコアを付与します
    model = LocalOutlierFactor(novelty=True, n_neighbors=n_neighbors,leaf_size=leaf_size,metric=metric) # ここではk-近傍法の流れをくむLocalOutlierFactorを用います
    model.fit(train_data) # 正常データのみを用いて学習します
    pred = model.score_samples(pred_data) 
    # 値が小さいほど異常度が高いので、符号をひっくり返して0から1におさめる
    scaler = MinMaxScaler()
    pred = scaler.fit_transform(-pred.reshape(-1,1))
    return pred
norm_idx = list(df[df['target']==0].index)
anorm_idx = list(df[df['target']==1].index)

tr_idx = norm_idx[:int(len(norm_idx)/2)]
val_idx = norm_idx[int(len(norm_idx)/2):] + anorm_idx
tr = features[tr_idx]
val = features[val_idx]
# LOFのn_neighborsを振ってcvスコア確認
for param in [5,10,15,20,25,30]:
    df_valid = df.copy()
    for leaf_size in [5,10,15,20,25,30]:
        for met in ["braycurtis", "canberra", "chebyshev", "correlation", "dice", "hamming", "jaccard", "kulsinski", "minkowski", "rogerstanimoto", "russellrao", "sokalmichener", "sokalsneath", "sqeuclidean", "yule"]:
            df_valid.loc[val_idx, 'pred'] = outlier_scoring(tr, val, param,leaf_size,met)
            df_valid = df_valid.dropna(subset=['target', 'pred'])
            score = roc_auc_score(df_valid['target'], df_valid['pred'])
            print(f'n_neighbors={param},leafsize={leaf_size},metric={met} | roc_auc_socre={score}')
tr_idx = norm_idx
pred_idx = list(df[df['file'].str.contains('test')].index)
tr = features[tr_idx]
pred = features[pred_idx]
df_pred = df.copy()
df_pred.loc[pred_idx, 'target'] = outlier_scoring(tr, pred,n_neighbors=25,leaf_size=20,metric="braycurtis" )
df_pred = df_pred[df_pred['file'].str.contains('test')]
# 1打音ごとの結果の平均取ってファイル単位の予測値を算出する
mean_pred = df_pred.groupby('id')['target'].mean().reset_index()
# sample_subに結果をmerge
sub = pd.read_csv('sample_submission.csv', usecols=['ID']).rename(columns={"ID":"id"})
sub = sub.merge(mean_pred, on='id', how='left')
sub=sub.rename(columns={"id":"ID","target":"Target"})
# すべて無音だった場合、予測値がないので平均値で埋める
sub = sub.fillna(sub['Target'].mean())
sub = sub.set_index('ID')
sub.to_csv('/kaggle/working/submission.csv')
sub
# librosaで.wavファイルから特徴抽出します。
n_features = 20

features = np.zeros((len(df), n_features))
for i in tqdm(range(len(df))):
    filepath = df.loc[i, 'filepath']
    st_time = df.loc[i, 'st_time']
    duration = df.loc[i, 'duration']
    
    y, sr = librosa.load(filepath, offset=st_time, duration=duration)
    #外れ値を考慮して
    y=scipy.stats.zscore(y)
    widths = np.arange(1, 85)
    mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20)
    ceps = mfcc.mean(axis=1)
    
    # 複数のローリングウィンドウでそれぞれ20次元のMFCCを得られるので、その平均をとる。
    features[i] = ceps
"""
#### **反省点**<br>
期待していたものよりあまり精度は変わらなかったようです…<br>
原因としては音声解析に対する知識不足や経験不足など色んな原因があると思います。
音声解析は大学院では学ぶ機会はありませんでしたので、今回のコンペを通して改めてまた勉強しておきたいと考えております。残り1週間弱ですが、引き続き改善を尽くしたいと思います。
"""