霞と側杖を食らう

ほしいものです。なにかいただけるとしあわせです。[https://www.amazon.jp/hz/wishlist/ls/2EIEFTV4IKSIJ?ref_=wl_share]

説明記事

■ 自己の説明

●  学んできたこと

・大学、大学院:経済学。統計学

プログラミング言語:RとSAS。ほんのすこしpython

● 関心事

・生物統計学(生存時間解析や縦断データ解析)、因果推論、疫学

● 資格

・統計検定1級、日本酒検定3級、さんま焼き師

■ 記事の説明

● タグの説明 (この説明記事には全てのタグを付けてある)

・学習記録:何かしら学んだことで記録すべきと思ったものを記録している。

・千文字語:千文字前後で何かしら書き留めたくなったことを記録している。

・衝動遊戯:衝動的に遊び感覚で作りたくなったものを記録している。

・八百万覚書:学習記録とまでは行かないが忘れたくないTipsを記録している。

・後日譚々:何かしらの後日にそのことについての追憶を記録している。

・R:プログラミング言語のRを使ったものに付けている。

 

● 代表的記事をいくつか(気に入っているやつ)

好きなモデルは?と問われたならば - 霞と側杖を食らう

リリーフ運用指標MRTRの作成の学習記録その1 - 霞と側杖を食らう

Immortal time biasに対する統計解析手法のシミュレーションの学習記録その1 - 霞と側杖を食らう

LaLonde(1986)とその周辺の学習記録 - 霞と側杖を食らう

Jリーグコンペの後日譚々 - 霞と側杖を食らう

 

■ 過去の制作物

● 公開中のスライド

・トピックモデルで1週間の献立をレコメンドする

speakerdeck.com

・バドミントン 戦略シミュレーション分析

speakerdeck.com

 

Bradley-Terryモデルによる戦闘力推定 - Speaker Deck

野球のリリーフ指標MRTRの開発をやってみた - Speaker Deck

医学論文を探そう ~{easyPubMed}パッケージの紹介~ / Introduction to easyPubMed - Speaker Deck

確率分布の紹介 - Speaker Deck

フェアな比較を崩すもの ~交絡と効果修飾~ / Confounding EffectModification - Speaker Deck

第1回 クイズ大会 問題 - Speaker Deck

第1回 クイズ大会 解答 - Speaker Deck

スクリーニング評価の注意点 - Speaker Deck

 

■ おしまい

科学哲学とその周辺に関する学習記録

【学習動機】

科学哲学に関心があって、科学哲学の入門書を何冊か買って積んでいたが、読む時間がとれたので、読んでみた。加えて、直接的には科学哲学の本ではないが、関連していそうな本を読んで面白かったので、それらについて、ここに記録しておく。

amazonのリンクを貼っているが、少し小銭稼ぎが必要な時期なので、アフィリエイトリンクを利用しています。

【学習内容】

科学哲学の入門書

・サミール・オカーシャ『哲学がわかる 科学哲学 新版』

哲学がわかる 科学哲学 新版 https://amzn.to/3PMO3xu

科学哲学に入門したくて、本文が160ページ程で少なめなので、一冊目として読んでみた。科学哲学関連でよく出てくるキーワードやテーマ(推論、説明、実在論、科学の変化)などがなんとなく把握できた。本文の後に、原著の読書案内に加えて、訳者による日本の読書案内があって次に繋げやすくて良かった。

戸田山和久『科学哲学の冒険 サイエンスの目的と方法をさぐる』

科学哲学の冒険 サイエンスの目的と方法をさぐる (NHKブックス) https://amzn.to/3vFh8nK

オカーシャの新版の入門書を読んだ後に、読んでみた。対話形式で書かれた科学哲学の入門書だが、著者は科学的実在論の立場で書いている。科学哲学の基礎概念を紹介した後、科学的実在論を導入し、その論の批判と、それに対する擁護を書いている。概念を整理できて良かった。最後の方で以前から個人的に関心があるモデルの話が少し出て来た。意味論をキーワードに追ってみるともう少し詳しく掘れそうだが、重たそうなので、それをするのはまたいつかにしておく。科学的実在論側以外の立場(反実在論と社会構成主義)から説明する書籍があったら読んでみたい。

伊勢田哲治疑似科学と科学の哲学』

疑似科学と科学の哲学 https://amzn.to/3U311tl

戸田山『科学哲学の冒険』を読んだ後に読んだ。科学と疑似科学をどう区別するかという問題(線引き問題、境界設定問題)を扱いながらも、科学哲学の入門本になっている。 哲学でよく出てくる分野の論理学、認識論、形而上学、価値理論(倫理学)に科学哲学の問題領域は渡っていて、それぞれの分野における論点を整理しつつ、創造科学、占星術超心理学代替医療などを例に評価しながら、線引き問題に対する答えを出している。著者による科学哲学日本語ブックガイド(https://tiseda.sakura.ne.jp/PofSbookguide.html)も公開されており、その後の学習の参考になりそう。

科学哲学の関連(してると思われる)本

・スティーヴン・シェイピンとサイモン・シャッファー 『リヴァイアサンと空気ポンプ―ホッブズ、ボイル、実験的生活―』

リヴァイアサンと空気ポンプ―ホッブズ、ボイル、実験的生活― https://amzn.to/3VM0GwE

実験により示されたことが正しいものとして認識されるのが普通になっているのはいつからなのか。そういう疑問があった学校の社会で学ぶところで、この本に出会った。ホッブズと理科で学ぶボイルが論争していたということで、つながるとは思わなかった。中身についてはこの記事(https://allreviews.jp/review/4894)が参考になる。 ボイルの実験による知識の生産様式が受け入れられていったのは、イギリスの王政復古期の政治的状況の要請に合っていたからとされている。科学社会学の流れが今どうなってるのか気になった。この本自体は分量が多いので、読むのは大変なので、もう少し軽くしたものに、追加でその後の流れが解説されている本があったら読んでみたいと思った。

・渡邉雅子『「論理的思考」の文化的基盤 4つの思考表現スタイル』

「論理的思考」の文化的基盤 4つの思考表現スタイル https://amzn.to/3vCYGMk

いのほた言語学チャンネルの動画で紹介されていて買ってみた。
それぞれちがう!アメリカ、フランス、イラン、日本の「論理」を比較!---渡邉雅子さん『「論理的思考」の文化的基盤』のご紹介・『納得の構造』も!【井上逸兵・堀田隆一英語学言語学チャンネル 第191回 】 - YouTube

教育原理の四類型に分けて、それぞれの代表としてアメリカ、フランス、イラン、日本の教育文化、思考表現スタイルを分析している。論理展開の仕方を作文や小論文の教育から、推論における因果や時間の解釈を歴史の教育から、その文化において何の能力に価値をおくかを大学入試から捉えていくアプローチを取っていて面白い。分類や分析の妥当性は自分には分からないが、各国の事例を見ているだけでも十分面白い。思考表現スタイルは裁判の国際比較にも何か出てくると思うし、研究もありそうだが、どうなのか気になった。

・マイケル・ワイスバーグ『科学とモデル―シミュレーションの哲学 入門―』

科学とモデル―シミュレーションの哲学 入門― https://amzn.to/3vO4r9Y

この本を読んだのは大分前だが、関係がありそうなので貼っておく。モデルを構造と解釈の組み合わせから成り立つとし、モデルの構造を具象モデル、数理モデル数値計算モデルの3種類に分類している。作者がシミュレーション寄りの人ではあるが、モデルとは何かを理解する一歩目になる。

野家啓一『物語の哲学』

物語の哲学 (岩波現代文庫 学術 139) https://amzn.to/3U72oYg

この本は物語に関して学んでいたときに読んでいた本だが、第五章の「物語と科学のあいだ」が科学と文学のグラデーションの話が書いてあって面白かったように思うので、メモのために貼っておく。

【学習予定】

科学哲学の各ジャンル 統計学の哲学や、因果、確率周りの哲学の本もいくつか積んでいるのだけど、まだ手をつけられなかった。時間が作れたら、それらの本を読んでまた記録したい。

美味しさの表現に関する学習記録

【学習動機】

料理やお酒に関心があり、この2,3年間、どのように表現すればいいのかを考えながら、関係する本などを読んでいた。日本酒については、日本酒検定3級を取ったときに記録を書いた。

moratoriamuo.hatenablog.com

日本酒の美味しさの表現については2年くらい記録をつけるようにしてきた。

美味しさを言語化しながらも、どう表現していいか分からなくなって困ることが多い。そういうわけで味の表現に関連する本を読んだりしていた。そういう経緯で、美味しさの表現に関して読んだ本などについて、ここに記録しておく。amazonのリンクを貼っているが、少し小銭稼ぎがしたいお年頃なので、アフィリエイトリンクを利用しています。

【学習内容】

・伏木亨『コクと旨味の秘密』

コクと旨味の秘密 (新潮新書) https://amzn.to/3wEYjAU

「コク」という表現は頻繫に聞くけれど、「コク」が何なのかよく分からないという人はいると思うし、実際に自分もよく分かっていなかった。この本では、「コク」の構造は多層的であるという。「コク」は三層構造で説明される。第一層は生命維持に必要な糖、油、ダシで説明される。第二層は第一層を色付ける、食感、香り、風味で説明される、学習によって得られた感覚で刺激されるものである。第三層は、実体のない精神性といったもので説明されている。「コク」というものは空間的、時間的、精神的な広がり、奥行きによる深みのある味と言えそうであるし、実際にそう感じる。この言語化を可能にした一冊だった。

玉村豊男『料理の四面体』

料理の四面体 (中公文庫) https://amzn.to/3UT7UOD

仏文学者が書いている料理の本。最初の半分は世界の料理の例示から始まるのだが、後半では、それらの世界の料理が一つの理論で位置付けられることを示す。その理論とは、どのような料理も、火、油、水、空気を頂点として成立する四面体のどこかの点に位置するとするものである。そのような切り口は斬新だし、実際にその四要素の加減で表現できそうだと実感する。新しい料理を考えるときに、その四要素の加減を変化したらどうなるかを考えてみれば、新しい領域に辿り着けるかもしれない。これが理論の強さである。

佐藤秀美『おいしさをつくる「熱」の科学―料理の加熱の「なぜ?」に答えるQ&A』

おいしさをつくる「熱」の科学―料理の加熱の「なぜ?」に答えるQ&A https://amzn.to/48uXcB9

Q&A形式の本。熱の伝わり方として対流、伝導、輻射、調理法として、ゆでる、煮る、焼く、炒める、煎る、オーブン焼き、直火焼きと始まる。温度帯や伝わる速度が、食材や使う器具によって適切なものが異なるということがよく分かる。麺を茹でるときのふきこぼれがどういう原理で起きるのかとか、二日目のカレーはどういう変化が起きているのかが個人的に印象的だった。料理は科学で、原理を知れば応用可能性が広がって、面白い。

・杉田浩一『「こつ」の科学―調理の疑問に答える』

「こつ」の科学―調理の疑問に答える https://amzn.to/49tWO7d

「熱」の科学よりは調理の仕方全般にわたる話が書かれている。レシピを読んで、なんとなく従っていることの理由が書かれていて、これまた面白い。

・伏木亨『味覚と嗜好のサイエンス』

味覚と嗜好のサイエンス [京大人気講義シリーズ] https://amzn.to/49tWPIj

『コクと旨味の秘密』の作者の著書。重なってくる部分もある。嗜好の構成要因は、①生理的な欲求が満たされるおいしさ、②食文化に合致したおいしさ、③情報がリードするおいしさ、④やみつきになる特定の食材が脳の報酬系を刺激する、としている。京大の講義を書き下ろしたものなのもあって、アラカルト的で読みやすい。

・チャールズ・スペンス『「おいしさ」の錯覚 最新科学でわかった、美味の真実』

「おいしさ」の錯覚 最新科学でわかった、美味の真実 https://amzn.to/49OfrCQ

イグノーベル賞を取った作者が書いている。その賞を取った研究は湿気ったポテチは、咀嚼音を拾って高周波のサクサク音を強調して返して音を流すと美味しく感じられるということを示している。
味というものが、舌の上で感じられる味覚だけでなく、他の感覚である視覚、聴覚、嗅覚、触覚に加え、雰囲気含めた情報に左右される。つまり、美味しさはあらゆる感覚に影響される。そのことの例示がたくさんある(示された例がどれくらい妥当なのかは分からないが)。音だとか香りだとか、食器の形や重さをはじめ、記憶や環境なども考慮して美味しさを考えている。ページは多いが、海外のレストラン等の例示が多いので、そのへんの名前を読み飛ばせば、意外とすぐに読み終えられる。

サンキュータツオ『国語辞典を食べ歩く』

国語辞典を食べ歩く https://amzn.to/3wrjYMV

岩波国語辞典、三省堂国語辞典新明解国語辞典明鏡国語辞典。小型の国語辞典で有名な四つの辞典で食べ物に関係する言葉を引き比べる。辞典によって、求める理想、思想が異なり、書き表し方が異なる。その違いを味わって読むのが面白い。

・瀬戸賢一(編集) 味ことば研究ラボラトリー(著) 『おいしい味の表現術』
おいしい味の表現術 (インターナショナル新書) https://amzn.to/3SOJfZb

味について言語表現から迫る本。各章を言語研究者が書いている。書いている人によってテーマが大きく異なり、マンガの分析もあったりして面白い。

・角直樹『おいしさの見える化 風味を伝えるマーケティング力』
おいしさの見える化 風味を伝えるマーケティングhttps://amzn.to/42WRIhp

おいしさの原理の説明と、おいしさの言語化の説明、マーケティングへの応用方法の説明がコンパクトにまとまっていて、良い一冊。マーケティングへの応用を目指す人向けの本だが、そうでない人にも役立つと思う。

・ゆる言語学ラジオの食レポシリーズ

言語学から考える食レポ。なぜソムリエは謎の語彙を使うのか?【食レポ1】#202 - YouTube

食べ物、飲み物の言語表現に関して参考になる。動画として面白い。

・板倉酒造の天穏の飲み方

天穏の飲み方 | 板倉酒造有限会社|島根県の酒蔵 天穏

天穏のページより引用している。このテイスティングの仕方の図解が分かりやすい。とく日本酒をどのように楽しんでみればいいかの参考になる。

【学習予定】

味覚表現について、まだまだ深いところへ行けていない。むしろ壁にぶち当たっているように思う。また何年かかかると思うが、どこか壁を壊すか越えられるようなことがあれば、また記録していきたい。

カプランマイヤー曲線からデータを復元する方法の学習記録(後編)

【学習動機】

前編からの続き. 

moratoriamuo.hatenablog.com

カプランマイヤー曲線から, 疑似的な個々のtime-to-eventデータを復元したいとする. 復元の手順としては, Kaplan-Meier curve(カプランマイヤー曲線)からGraph Digitization Software(グラフ数値化ソフトウェア)を使ってグラフ上の各軸の値を取得して, その取得した値をGuyot et al. (2012) の提案したアルゴリズムを使って, 個々のtime-to-eventデータを復元していくことなる. 前編の記事では, digitize(digitise, 数値化)の方法について書いた.

今回の後編の記事では数値化したデータを使って, Guyot et al. (2012)の方法で復元していく方法について書く. コードはRを使う. Stataでやってみた文献( https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5796634/ )もあるが, 今回は扱わない.

 

【学習内容】

Guyot et al. (2012)はこちら. Supplementary MaterialsにRのコードがある. 

www.ncbi.nlm.nih.gov

Rのコードに合うように, 自分で前処理をしていく. このやり方が完全に正しい保証はないので, 使う場合は元文献にもあたって検証してから使用してください.

 

パッケージの読み込み.

library(tidyverse)
library(survival)
library(survminer)

前回の記事で作成したデータ(KM.csv)を読み込んで, 曲線の横棒の左端のみのデータになるように絞った後, それぞれの座標が, どの区間に該当するのかを変数(interval)にする. 最後に各座標に番号を付けておく.

df <- read_csv("KM.csv",
               col_names = c("Time", "Prob"),
               show_col_types = FALSE)

df2 <- df %>% 
  filter(row_number()%%2==1) %>% 
  mutate(
    interval = case_when(
      Time >= 0 & Time < 250 ~ 0,
      Time >= 250 & Time < 500 ~ 250,
      Time >= 500 & Time < 750 ~ 500,
      Time >= 750 & Time < 1000 ~ 750,
      Time >= 1000 ~ 1000
    ),
    rn = row_number()
  )

 

対象の文献でNumber at Riskの情報を読み込ませる.

df_NatRisk <- data.frame(
  interval = 1:5,
  Time = c(0, 250, 500, 750, 1000),
  NatRisk = c(228, 115, 41, 10, 2)
)

Guyot et al.(2012)のSupplementary Materialsのコードを改変しながら利用.

総イベント数が手に入る場合は記入する. 今回は総イベント数が分かっている(169)として扱う. 分からない場合はNAにする. 分かっている場合の方がもちろん復元の精度は良くなる. arm.idは今回は群分けしていないので, allとしている.

# 総イベント数が分からない場合はNA
tot.events <- "NA" #tot.events = total no. of events reported. If not reported, then tot.events="NA"
tot.events <- "169" # 総イベント数が分かる場合は記入
arm.id <- "all" #arm indicator

出来上がったデータセットcsvファイルにするために, 名前を決めておく.

###FUNCTION INPUTS
KMdatafile <- "KMdata.csv" #Output file events and cens
KMdataIPDfile <- "KMdataIPD.csv" #Output file for IPD

元のコードに合わせるため, 変数を作成(もっと整理できそうだけれど今回は実行重視ということで).

t.S <- df2$Time
S <- df2$Prob
pub.risk <- df_NatRisk %>% 
  rename(
    t.risk = Time,
    n.risk = NatRisk
  ) 
t.risk <- pub.risk$t.risk
n.risk <- pub.risk$n.risk

Number at Riskの各区間に含まれるクリックの番号のlowerとupperの値を計算する. これはGuyot et al.(2012)のTable 2の不足分に該当. このあたりの計算は場当たり的に作成しているので, 状況に合わせて改変する必要があるかもしれない.

# 
lower <- numeric(nrow(pub.risk))
upper <- numeric(nrow(pub.risk))

for(i in 1:(length(t.risk)-1)){
  intvl <- 
    df2 %>% mutate(l = t.risk[i],
                   u = t.risk[i+1]) %>% 
    filter(l<=Time & Time<u)
  lower[i] <- min(intvl$rn)
  upper[i] <- max(intvl$rn)
  
  # 最後の区間だけ例外処理
  intvl <- 
    df2 %>% mutate(u=t.risk[i+1]) %>% 
    filter(u<=Time)
  lower[i+1] <- min(intvl$rn)
  upper[i+1] <- max(intvl$rn)
}
## Warning in min(intvl$rn): min の引数に有限な値がありません: Inf を返します
## Warning in max(intvl$rn): max の引数に有限な値がありません: -Inf を返します

Guyot et al.(2012)のTable 2を作成しなおす. ただし, この計算の仕方だと, たとえば, イベントが全く起きない区間があると, 上手く行かないことがあるので, そのときは臨機応変に対応する必要がある.

pub.risk <- 
  pub.risk %>% mutate(lower=lower, upper=upper) %>% 
  filter(!is.infinite(lower)) %>% 
  filter(!is.infinite(upper))
pub.risk
##   interval t.risk n.risk lower upper
## 1        1      0    228     1    56
## 2        2    250    115    57    92
## 3        3    500     41    93   114
## 4        4    750     10   115   118

アルゴリズムに必要な値の作成.

t.risk <- pub.risk$t.risk
n.risk <- pub.risk$n.risk
lower <- pub.risk$lower
upper <- pub.risk$upper

n.int <- length(n.risk)
n.t <- upper[n.int]

準備はできたので, Guyot et al.(2012)のアルゴリズムに従う.

#Initialise vectors
arm <- rep(arm.id,n.risk[1])
n.censor <- rep(0,(n.int-1))
n.hat <- rep(n.risk[1]+1,n.t)
cen <- rep(0,n.t)
d <- rep(0,n.t)
KM.hat <- rep(1,n.t)
last.i <- rep(1,n.int)
sumdL <- 0

if (n.int > 1){
  #Time intervals 1,...,(n.int-1)
  for (i in 1:(n.int-1)){
    #First approximation of no. censored on interval i
    n.censor[i] <- round(n.risk[i]*S[lower[i+1]]/S[lower[i]]- n.risk[i+1])
    #Adjust tot. no. censored until n.hat = n.risk at start of interval (i+1)
    while((n.hat[lower[i+1]]>n.risk[i+1])||((n.hat[lower[i+1]]<n.risk[i+1])&&(n.censor[i]>0))){
      if (n.censor[i]<=0){
        cen[lower[i]:upper[i]] <- 0
        n.censor[i] <- 0
      }
      if (n.censor[i]>0){
        cen.t<-rep(0,n.censor[i])
        for (j in 1:n.censor[i]){
          cen.t[j] <- t.S[lower[i]] +
            j*(t.S[lower[(i+1)]]-t.S[lower[i]])/(n.censor[i]+1)
        }
        #Distribute censored observations evenly over time. Find no. censored on each time interval.
        cen[lower[i]:upper[i]]<-hist(cen.t,breaks=t.S[lower[i]:lower[(i+1)]],
                                     plot=F)$counts
      }
      #Find no. events and no. at risk on each interval to agree with K-M estimates read from curves
      n.hat[lower[i]]<-n.risk[i]
      last <- last.i[i]
      for (k in lower[i]:upper[i]){
        if (i==1 & k==lower[i]){
          d[k] <- 0
          KM.hat[k] <- 1
        }
        else {
          d[k] <- round(n.hat[k]*(1-(S[k]/KM.hat[last])))
          KM.hat[k] <- KM.hat[last]*(1-(d[k]/n.hat[k]))
        }
        n.hat[k+1] <- n.hat[k]-d[k]-cen[k]
        if (d[k] != 0) last<-k
      }
      n.censor[i] <- n.censor[i]+(n.hat[lower[i+1]]-n.risk[i+1])
    }
    if (n.hat[lower[i+1]]<n.risk[i+1]) n.risk[i+1]<-n.hat[lower[i+1]]
    last.i[(i+1)] <- last
  }
}

#Time interval n.int.
if (n.int>1){
  #Assume same censor rate as average over previous time intervals.
  n.censor[n.int] <- min(round(sum(n.censor[1:(n.int-1)])*(t.S[upper[n.int]]-
                                                            t.S[lower[n.int]])/(t.S[upper[(n.int-1)]]-t.S[lower[1]])), n.risk[n.int])
}
if (n.int==1){n.censor[n.int]<-0}
if (n.censor[n.int] <= 0){
  cen[lower[n.int]:(upper[n.int]-1)]<-0
  n.censor[n.int] <- 0
}
if (n.censor[n.int]>0){
  cen.t<-rep(0,n.censor[n.int])
  for (j in 1:n.censor[n.int]){
    cen.t[j] <- t.S[lower[n.int]] +
      j*(t.S[upper[n.int]]-t.S[lower[n.int]])/(n.censor[n.int]+1)
  }
  cen[lower[n.int]:(upper[n.int]-1)] <- hist(cen.t,breaks=t.S[lower[n.int]:upper[n.int]],
                                           plot=F)$counts
}
#Find no. events and no. at risk on each interval to agree with K-M estimates read from curves
n.hat[lower[n.int]] <- n.risk[n.int]
last<-last.i[n.int]
for (k in lower[n.int]:upper[n.int]){
  if(KM.hat[last] !=0){
    d[k] <- round(n.hat[k]*(1-(S[k]/KM.hat[last])))} else {d[k]<-0}
  KM.hat[k] <- KM.hat[last]*(1-(d[k]/n.hat[k]))
  n.hat[k+1] <- n.hat[k]-d[k]-cen[k]
  #No. at risk cannot be negative
  if (n.hat[k+1] < 0) {
    n.hat[k+1] <- 0
    cen[k] <- n.hat[k] - d[k]
  }
  if (d[k] != 0) last<-k
}
#If total no. of events reported, adjust no. censored so that total no. of events agrees.
if (tot.events != "NA"){
  if (n.int>1){
    sumdL <- sum(d[1:upper[(n.int-1)]])
    #If total no. events already too big, then set events and censoring = 0 on all further time intervals
    if (sumdL >= tot.events){
      d[lower[n.int]:upper[n.int]] <- rep(0,(upper[n.int]-lower[n.int]+1))
      cen[lower[n.int]:(upper[n.int]-1)] <- rep(0,(upper[n.int]-lower[n.int]))
      n.hat[(lower[n.int]+1):(upper[n.int]+1)] <- rep(n.risk[n.int],(upper[n.int]+1-lower[n.int]))
    }
  }
  #Otherwise adjust no. censored to give correct total no. events
  if ((sumdL < tot.events)|| (n.int==1)){
    sumd<-sum(d[1:upper[n.int]])
    while ((sumd > tot.events)||((sumd< tot.events)&&(n.censor[n.int]>0))){
      n.censor[n.int] <- n.censor[n.int] + (sumd - tot.events)
      if (n.censor[n.int]<=0){
        cen[lower[n.int]:(upper[n.int]-1)]<-0
        n.censor[n.int] <- 0
      }
      if (n.censor[n.int]>0){
        cen.t <- rep(0,n.censor[n.int])
        for (j in 1:n.censor[n.int]){
          cen.t[j] <- t.S[lower[n.int]] +
            j*(t.S[upper[n.int]]-t.S[lower[n.int]])/(n.censor[n.int]+1)
        }
        cen[lower[n.int]:(upper[n.int]-1)]<-hist(cen.t,breaks=t.S[lower[n.int]:upper[n.int]],
                                                 plot=F)$counts
      }
      n.hat[lower[n.int]]<-n.risk[n.int]
      last <- last.i[n.int]
      for (k in lower[n.int]:upper[n.int]){
        d[k] <- round(n.hat[k]*(1-(S[k]/KM.hat[last])))
        KM.hat[k] <- KM.hat[last]*(1-(d[k]/n.hat[k]))
        if (k != upper[n.int]){
          n.hat[k+1] <- n.hat[k]-d[k]-cen[k]
          #No. at risk cannot be negative
          if (n.hat[k+1] < 0) {
            n.hat[k+1] <- 0
            cen[k] <- n.hat[k] - d[k]
          }
        }
        if (d[k] != 0) last<-k
      }
      sumd <- sum(d[1:upper[n.int]])
    }
  }
}

write.csv(matrix(c(t.S,n.hat[1:n.t],d,cen),ncol=4,byrow=F),
          paste("output/",KMdatafile,sep=""))

### Now form IPD ###
#Initialise vectors
t.IPD <- rep(t.S[n.t],n.risk[1])
event.IPD <- rep(0,n.risk[1])
#Write event time and event indicator (=1) for each event, as separate row in t.IPD and event.IPD
k=1
for (j in 1:n.t){
  if(d[j]!=0){
    t.IPD[k:(k+d[j]-1)] <- rep(t.S[j],d[j])
    event.IPD[k:(k+d[j]-1)] <- rep(1,d[j])
    k <- k+d[j]
  }
}
#Write censor time and event indicator (=0) for each censor, as separate row in t.IPD and event.IPD
for (j in 1:(n.t-1)){
  if(cen[j]!=0){
    t.IPD[k:(k+cen[j]-1)] <- rep(((t.S[j]+t.S[j+1])/2),cen[j])
    event.IPD[k:(k+cen[j]-1)] <- rep(0,cen[j])
    k <- k+cen[j]
  }
}

復元によって得られた, 疑似的なtime-to-eventデータの個票データを作成する.

#Output IPD
IPD <- matrix(c(t.IPD,event.IPD,arm),ncol=3,byrow=F)
#Find Kaplan-Meier estimates
IPD <- IPD %>% as.data.frame() %>% 
  set_names(c("time","event","arm")) %>% 
  mutate(time=as.numeric(time),
         event=as.numeric(event))

write.csv(IPD, str_c("output/",KMdataIPDfile), row.names = FALSE)

疑似的な個票データはこのような感じ.

head(IPD)
##        time event arm
## 1  5.847953     1 all
## 2  5.847953     1 all
## 3  5.847953     1 all
## 4 11.695906     1 all
## 5 13.157895     1 all
## 6 14.619883     1 all

復元によって得られたデータでカプランマイヤー推定をする. 総イベント数を指定しているので一致する. 指定できないと一致するとは限らない.

KM.est <- survfit(Surv(time,event)~1,
                data = IPD,
                type = "kaplan-meier",)
KM.est
## Call: survfit(formula = Surv(time, event) ~ 1, data = IPD, type = "kaplan-meier")
## 
##        n events median 0.95LCL 0.95UCL
## [1,] 228    169    310     285     364

曲線を描いてみる.

ggsurvplot(KM.est, 
           data = IPD,
           conf.int = FALSE,
           risk.table = TRUE)

元データのカプランマイヤー曲線は 

であり, もちろん完全再現ではないが, 近いものが復元できている(と思う).

最後に繰り返しになるが, このやり方が完全に正しい保証はないので, 使う場合は元文献にもあたって検証してから使用してください.

【学習予定】

Rには{reconstructKM}というパッケージがあるが, こちらはあまり読み込んでいない. https://cran.r-project.org/web/packages/reconstructKM/index.html

他にもっといいやり方があるかもしれないので, 見つけたら学習する.

 

カプランマイヤー曲線からデータを復元する方法の学習記録(前編)

【学習動機】

論文に描かれているKaplan-Meier curve(カプランマイヤー曲線)からtime-to-eventデータを再現したいということがある. たとえば, meta-analysis(メタアナリシス)やcost-effectiveness analysis(費用効果分析), その他の二次的な分析をしたいときなどである. このようなときに, time-to-eventデータを復元する方法として, よく見かけるのが, Guyot et al. (2012) [Enhanced secondary analysis of survival data: reconstructing the data from published Kaplan-Meier survival curves]の方法である. この方法について, 具体的に解説している日本語の文献や記事が見当たらなかったので, ここに書き留めておくことにした.

復元の手順としては, Kaplan-Meier curve(カプランマイヤー曲線)からGraph Digitization Software(グラフ数値化ソフトウェア?)を使ってグラフ上の各軸の値を取得して, その取得した値をGuyot et al. (2012) の提案したアルゴリズムを使って, 個々のtime-to-eventデータを復元していくことなる. この記事では, 前半のdigitize(digitise, 数値化)の方法について書き, 後半のGuyot et al. (2012)の方法については別の記事で書く.

【学習内容】

グラフから数値を読み取るためのdigitizeのソフトウェアは複数あって, どれを使うと良いか悩んでいたら, Matsumotoらのポスターで, “A Comparison of Graph Digitization Software for the Reconstruction of Published Kaplan Meier Curves”というものを発見した.

[https://www.valueinhealthjournal.com/article/S1098-3015(20)33910-3/fulltext]

[https://www.ispor.org/docs/default-source/euro2020/ispor-eu-2020-posterdigitization-pdf.pdf?sfvrsn=8d602d2f_0] (pdf注意)

自分の環境や条件, 目的に合わせて, 選択すれば良さそうである. この記事では, OSがwindowsで, ブラウザ上でも使えるWebPlotDigitizerを使うことにした.

[WebPlotDigitizer - Extract data from plots, images, and maps]

(ちなみに, Guyot et al. (2012)では, DigitizeItが使われている.)

基本的には, Tutorialを眺めれば良いのだけれど, 以下でやり方を書いていく. digitizeの対象とするカプランマイヤー曲線をどれにしようか考えて, 色々論文を検索したのだが, 例にしたら面白そうと思ったものがことごとく有料文献ばかりだったので, 結局, Rの{survival}パッケージに含まれるlungデータセットを使うことにした. カプランマイヤー曲線も自作する.

パッケージの読み込み.

library(tidyverse)
library(survival)
library(survminer)

カプランマイヤー曲線の作成.

fit <- survfit(Surv(time, status) ~ 1, data = lung)

p <- ggsurvplot(fit, 
                data = lung,
                conf.int = FALSE,
                risk.table = TRUE)
p  

図の保存.

ggsave(file = "KM.png", 
       plot = p$plot,
       width = 12,
       height = 4)

保存したWebPlotDigitizerでFileのLoad Imageから保存した画像をロードし, 2D plotを選択する. 次に, X軸とY軸の値の基準となる点を各軸2点ずつ選択する. これらの点から座標が設定されることになる. 選択したら, Completeを押す.

次に左上のTime=0, Probability=1から, カプランマイヤー曲線の曲がり角を選択していく. Add pointやDelete pointなどを駆使しながら, 左側から順番に押していく. 打ち切りの縦線は無視する. カプランマイヤー曲線の画像が荒かったり, 複数曲線があって重なってたりすると大変だったりする. 狙った点をクリックするためにマウスの反応速度を調整したりすると良いかと思う.

ちなみに, Guyot et al. (2012)の方法に必要なのは, 横線の左端の点があればよく, 右端は要らないのだが, 右端も取っておくと, 後で得られたデータをプロットするときにカプランマイヤー曲線を再現できるので, 一緒に取ってしまっている.

必要な点をクリックし終えたら, View Dataからデータを落とす.

 

取得したデータをRで読み込んで確認する. 

df <- read_csv("KM.csv",
               col_names = c("Time", "Prob"),
               show_col_types = FALSE)

プロットして確認.

ggplot(df, aes(x=Time, y=Prob)) +
  geom_line() +
  theme_bw()

画像の粗さとかから復元に限界はあるけれど, ある程度似たような曲線が得られたと思う.

取得したデータの加工やNumber at riskのデータ作成は後半の記事で行う.

【学習予定】

後半のGuyot et al. (2012)の方法についての記事を書く. 書いたらアップする.

 

 

2023年の後日譚々

2024年になったので、2023年を振り返る。昨年は美術館へ行く機会が例年より多かった。過去最多だった気さえする。国立西洋美術館とポーラ美術館でそれぞれピカソ展を見ることから始まった。それから行けたのは、東京都美術館エゴン・シーレ展と東京ステーションギャラリー佐伯祐三展、府中市美術館の諏訪敦展、根津美術館の燕子花図屏風、太田記念美術館葛飾応為の吉原格子先之図、川崎浮世絵ギャラリーの東海道五十三次展、平塚市美術館の玉田多紀展などなど。箱根彫刻の森美術館菊池寛実記念智美術館、東京都写真美術館も行ったし、岡靖知のギャラリーでの展示にも行けた。漫画の『ギャラリーフェイク』も読み終わった。アートへの触れ合いが充実した1年だった。

2023年も映画を50本見ることを目標にしていたのだが、結果55本見ていて、目標を達成することができた。映画館で見たものだと、圧倒的に良かったのが『BLUE GIANT』。次点で、『バビロン』や『怪物』あたりが面白かった。面白さとは別の視点になるが、年末に渋谷のシアター・イメージフォーラムで見た『プリズン・サークル』と『オオカミの家』はどちらも衝撃的だった。配信も含めると、『ロブスター』や『メタモルフォーゼの縁側』は印象に残っている。50本見ることは4年間継続できたが、2024年は忙しくなりそうなので、今年の50本は努力目標ということにする。

趣味として読んでいた本は、日本酒や味覚に関連する本、公衆衛生に関連する本、物語論に関連する本などを読んでいた。2024年は編集や科学哲学あたりをテーマに読み進める予定だ。あと、『三体』を昨年買って積んだままなので、早いうちに読まないといけない。漢検準1級の勉強や辞書の読み進めをしたかったのだが、優先度の関係で結局できなかった(9月は丸々ゼルダの伝説のティアキンで世界を救うのに奔走していたのもある)。これは2024年も進められそうにない気がするので保留する。

本の読み方について、人によって違うという話を以前書いた(本について語るとき某の語ること - 霞と側杖を食らう)が、年末に『アファンタジア―イメージのない世界で生きる』という本を読んで、自分がアファンタジア、つまり、頭の中でリアルで見るようにイメージを思い浮かべられない症状なのかもしれないということに気付いた。頭の中の動きが人によって違うとは思っていたが、自分がマイノリティー側かもしれないと気付いて少し衝撃を受けた。この話の続きはそのうち書くと思う。

2023年もたくさん日本酒を飲んだ。出会えて良かったのが、荷札酒と寒菊だ。この2銘柄は新しいのを見つけたら基本的に買っていた。最も美味しいと感じたのは荷札酒の紅桔梗だった。荷札酒の甘旨さと花のような香りの広がりが絶妙なバランスで最高に美味しかった。ある程度の決まった味わい方ができてきたが、もっと解像度を上げていきたい。日本酒検定の3級は取れたが、2級の勉強をする余裕は無かったので、今年どこかで時間が取れたら、取得したいところだ。

勉強面では、疫学や公衆衛生は基本的なところは一通り読めた。そして、進む道が決まった(この詳細はそのうち書く)。生存時間解析と縦断データ解析を組み合わせたジョイントモデルも学んだ(この詳細もそのうち書く)。今年は腰を据えて学ぶ時間が作れると思うので、そうしないと修得できないものを身に付けていこうと思う。テクノロジーに置いて行かれている危機感があり、人工知能の関連技術のキャッチアップもしておきたい。未曾有の出来事が頻繫に起こる激動の時代だが、何をして、何をしないか、曇りなき眼で見定め、決める。30年生きてみても、人生というものはよく分からないのだが、着実に一歩一歩、前に進めていく。それだけ。

日本酒アプリのさけのわデータの学習記録

 

【はじめに】

この記事は, さけのわデータ ( https://sakenowa.com )のデータを加工して利用しています. ありがとうございます!!

【学習動機】

飲んだ日本酒を記録して好みの日本酒を見つけることができるサービスの, さけのわ( https://sakenowa.com/ )のデータが無料で利用できるということは何年か前に知ってはいたのだが, なかなか利用しないままでいた. この数年で日本酒を飲んだり, 日本酒に関する本を読んだりすることで, 日本酒のドメイン知識が増えたので, それを, さけのわデータで利活用してみたいと思い立った. 個人的に最近, 日本酒の酸味に関心があるので, 酸味に関して何かしら分類ができないかと思っている. 今回の記事では, その分析をする前に, さけのわデータがどのようなデータ構造になっているのかを確かめることにする. データを扱う際にはRを使用する. 

【学習記録】

公式のさけのわデータの説明を確認する ( https://muro.sakenowa.com/sakenowa-data/ ) . json形式でデータが使えて, エンコーディングUTF-8となっている.

json形式のデータの扱いは詳しくないので, データをRで揃えてくれている先人はいないものかと探していたら, 先人の記事を発見した. このブログにRでデータを取得する方法が書かれていたので参考にしました. 今回のデータ取得のコードはほとんどをこのブログのコードに頼っています(ありがとうございます!!).

( https://stat-you1025.blogspot.com/2020/09/blog-post_18.html )

stat-you1025.blogspot.com

データを取得してみたので, 7つのデータの構造を確認する. 説明での括弧内は手元でのデータフレーム名や変数名を書いている. (データ取得のところは簡単に関数化しようと思ったのだけれど, 名前の不一致や格納されているデータの数の不一致などがあって, 途中で断念した.)
まずは, 必要なパッケージ読み込み. 

library(tidyverse)
library(httr)
library(jsonlite)

さて, 地域一覧, 銘柄一覧, 蔵元一覧, ランキング, フレーバーチャート, フレーバータグ一覧, 銘柄ごとのフレーバータグのデータを取得し, 確認していく.

1. 地域一覧

areas(df_areas)
- id(areaId)
- name(areaname)

df_areas <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/areas") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$areas %>%
  tibble::as_tibble() %>% 
  rename(areaId=id,
         areaname=name)

df_areas
## # A tibble: 48 x 2
##    areaId areaname
##     <int> <chr>   
##  1      1 北海道  
##  2      2 青森県  
##  3      3 岩手県  
##  4      4 宮城県  
##  5      5 秋田県  
##  6      6 山形県  
##  7      7 福島県  
##  8      8 茨城県  
##  9      9 栃木県  
## 10     10 群馬県  
## # ... with 38 more rows

48個データがあるので気になって確認してみたら, 47都道府県とその他がareaId 0番で入っていた.

2. 銘柄一覧

brands(df_brands)
- id(df_brands)
- name(brandname)
- breweryId

df_brands <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/brands") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$brands %>%
  tibble::as_tibble() %>% 
  rename(brandId=id,
         brandname=name)

df_brands
## # A tibble: 3,142 x 3
##    brandId brandname    breweryId
##      <int> <chr>            <int>
##  1       1 新十津川             1
##  2       2 男山                 2
##  3       3 大雪乃蔵             3
##  4       4 宝川                 4
##  5       5 亀甲蔵               4
##  6       7 北の誉               3
##  7       8 風のささやき         6
##  8       9 えぞ乃熊             6
##  9      10 国士無双             6
## 10      11 北の錦               7
## # ... with 3,132 more rows

3. 蔵元一覧

breweries(df_breweries)
- id(breweryId)
- name(breweryname)
- areaId

df_breweries <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/breweries") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$breweries %>%
  tibble::as_tibble() %>% 
  rename(breweryId=id,
         breweryname=name)

df_breweries
## # A tibble: 1,729 x 3
##    breweryId breweryname areaId
##        <int> <chr>        <int>
##  1         1 金滴酒造         1
##  2         2 男山             1
##  3         3 合同酒精         1
##  4         4 田中酒造         1
##  5         5 北の誉酒造       1
##  6         6 高砂酒造         1
##  7         7 小林酒造         1
##  8        10 八戸酒造         2
##  9        12 桃川             2
## 10        13 松緑酒造         2
## # ... with 1,719 more rows

1729の酒蔵が登録されていた. 酒類製造業及び酒類卸売業の概況(令和5年アンケート) ( https://www.nta.go.jp/taxes/sake/shiori-gaikyo/seizo_oroshiuri/r05/index.htm ) の酒類製造者等及び酒類卸売業者の概要の表1を確認してみると, 清酒の製造場数は1544とある. 注1を見ると複数品目を製造していると数量が最も多い品目で計上している. その他, 既になくなった蔵も考えれば, ほとんど全てカバーされているのかと思われる.

都道府県別の蔵元の数が気になったので確認.

df_breweries %>% 
  left_join(df_areas,by = "areaId") %>% 
  group_by(areaname) %>% 
  summarise(n=n()) %>% 
  arrange(-n)
## # A tibble: 48 x 2
##    areaname     n
##    <chr>    <int>
##  1 新潟県     113
##  2 兵庫県      96
##  3 長野県      91
##  4 山形県      76
##  5 福島県      69
##  6 岐阜県      54
##  7 福岡県      54
##  8 京都府      50
##  9 広島県      50
## 10 秋田県      48
## # ... with 38 more rows

4. ランキング

ランキングデータは3つあって, yearMonth(ランキングの対象年月), overall(総合ランキング), areas(地域ランキング).

yearMonth
ランキングの対象年月がYYYYMM形式で入っている.

rankings_YM <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/rankings") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$yearMonth

rankings_YM
## [1] "202309"

overall(df_rankings_overall)
- rank
- score
- brendId

df_rankings_overall <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/rankings") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$overall %>%
  tibble::as_tibble()

このままだと分からないので, 紐づけてランキングを確認.

df_rankings_overall %>% 
  left_join(df_brands, by="brandId")
## # A tibble: 100 x 5
##     rank score brandId brandname breweryId
##    <int> <dbl>   <int> <chr>         <int>
##  1     1  4.41     109 新政             76
##  2     2  4.10     144 十四代           96
##  3     3  4.09     792 風の森          589
##  4     4  4.09    1033 作              835
##  5     5  4.08     660 而今            488
##  6     6  4.08     258 鳳凰美田        183
##  7     7  4.07    1121 仙禽           1168
##  8     8  4.06      19 田酒             16
##  9     9  4.05    2602 赤武            898
## 10    10  4.04     975 鍋島            758
## # ... with 90 more rows

新政, 十四代, 風の森と有名な銘柄がずらりと並んでいる.

areas(df_rankings_areas)
- areaId
- ranking(中にrank, score, brandIdの情報が格納されている)

df_rankings_areas <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/rankings") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$areas %>%
  tibble::as_tibble()
df_rankings_areas
## # A tibble: 48 x 2
##    areaId ranking          
##     <int> <list>           
##  1      0 <df[,3] [2 x 3]> 
##  2      1 <df[,3] [25 x 3]>
##  3      2 <df[,3] [30 x 3]>
##  4      3 <df[,3] [30 x 3]>
##  5      4 <df[,3] [45 x 3]>
##  6      5 <df[,3] [50 x 3]>
##  7      6 <df[,3] [50 x 3]>
##  8      7 <df[,3] [50 x 3]>
##  9      8 <df[,3] [29 x 3]>
## 10      9 <df[,3] [29 x 3]>
## # ... with 38 more rows

ネスト化されているので, unnestで解除して, 他のデータを紐づけて, 神奈川県のランキングを確認してみる.

df_rankings_areas %>% 
  unnest(cols = everything()) %>% 
  left_join(df_areas, by="areaId") %>% 
  left_join(df_brands, by="brandId") %>% 
  filter(areaname=="神奈川県")
## # A tibble: 20 x 7
##    areaId  rank score brandId areaname brandname  breweryId
##     <int> <int> <dbl>   <int> <chr>    <chr>          <int>
##  1     14     1  4.41     323 神奈川県 いづみ橋         231
##  2     14     2  4.09    1022 神奈川県 天青            1071
##  3     14     3  4.06    1584 神奈川県 残草蓬莱         235
##  4     14     4  4.05     324 神奈川県 相模灘           232
##  5     14     5  4.05    1671 神奈川県 松みどり         942
##  6     14     6  4.02    1527 神奈川県 隆               237
##  7     14     7  4.02     326 神奈川県 昇龍蓬莱         235
##  8     14     8  4.02    1677 神奈川県 泉橋             231
##  9     14     9  4.01     327 神奈川県 丹沢山           237
## 10     14    10  4.01   66397 神奈川県 雨降            1144
## 11     14    11  4.01     325 神奈川県 白笹鼓           233
## 12     14    12  4.01    2107 神奈川県 盛升            1065
## 13     14    13  4.01    2867 神奈川県 黒とんぼ         231
## 14     14    14  4.01   39164 神奈川県 HINEMOS         1859
## 15     14    15  4.01    2424 神奈川県 箱根山          1125
## 16     14    16  4.01    1829 神奈川県 松美酉           942
## 17     14    17  4.01    3422 神奈川県 鎌倉栞          1071
## 18     14    18  4.01    3417 神奈川県 湘南            1071
## 19     14    19  4.01    3386 神奈川県 箱根街道        1012
## 20     14    20  4.01    3013 神奈川県 四季の箱根       942

いづみ橋や天青, 残草蓬莱(ざるそうほうらい)はたしかに飲んだことがある. そういえば, スコアがどのように付けられているのかが分からないので気になった. さけのわのランキングページを見てみると, 「さけのわでの人気の度合いをお気に入り、殿堂入りと最近のチェックイン数を元にした独自のアルゴリズムにより算出したスコア順で表示しています。」とあるからスコアは非公開の算出方法は非公開と思われる.

5. フレーバーチャート

flavorCharts(df_flavor_charts)
- brandId
- f1(hanayaka) : 華やかのスコア
- f2(hojun) : 芳醇のスコア
- f3(juko) : 重厚のスコア
- f4(odayaka) : 穏やかのスコア
- f5(dry) : ドライのスコア
- f6(keikai) : 軽快のスコア

df_flavor_charts <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/flavor-charts") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$flavorCharts %>%
  tibble::as_tibble() %>% 
  rename(hanayaka=f1,
         hojun=f2,
         juko=f3,
         odayaka=f4,
         dry=f5,
         keikai=f6)
df_flavor_charts
## # A tibble: 1,322 x 7
##    brandId hanayaka hojun  juko odayaka   dry keikai
##      <int>    <dbl> <dbl> <dbl>   <dbl> <dbl>  <dbl>
##  1       2    0.267 0.502 0.328   0.424 0.463  0.419
##  2       3    0.340 0.394 0.184   0.542 0.466  0.429
##  3       4    0.467 0.441 0.295   0.314 0.278  0.569
##  4       7    0.213 0.481 0.265   0.680 0.337  0.278
##  5       9    0.436 0.526 0.293   0.417 0.259  0.455
##  6      10    0.334 0.474 0.332   0.447 0.417  0.424
##  7      11    0.309 0.548 0.441   0.492 0.262  0.314
##  8      12    0.503 0.462 0.216   0.275 0.402  0.500
##  9      14    0.219 0.354 0.241   0.711 0.411  0.308
## 10      17    0.372 0.536 0.384   0.473 0.294  0.342
## # ... with 1,312 more rows

6. フレーバータグ一覧

tags(df_tags)
- id(tagIds)
- tag

df_tags <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/flavor-tags") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$tags %>%
  tibble::as_tibble() %>% 
  rename(tagIds=id)
df_tags
## # A tibble: 141 x 2
##    tagIds tag       
##     <int> <chr>     
##  1      2 酸味      
##  2      3 辛口      
##  3      5 旨味      
##  4      6 フルーティ
##  5      7 スッキリ  
##  6      9 しっかり  
##  7     12 甘味      
##  8     16 穀物      
##  9     17 苦味      
## 10     19 コク      
## # ... with 131 more rows

タグの種類のマスタになっていて, 次の銘柄ごとのフレーバータグで使われている.

7. 銘柄ごとのフレーバータグ

flavor
- brandId
- tagIds

df_flavortags <-
  httr::GET("https://muro.sakenowa.com/sakenowa-data/api/brand-flavor-tags") %>%
  httr::content(as = "text", encoding = "utf-8") %>%
  jsonlite::fromJSON() %>%
  .$flavorTags %>%
  tibble::as_tibble()
df_flavortags
## # A tibble: 2,800 x 2
##    brandId tagIds    
##      <int> <list>    
##  1       1 <int [0]> 
##  2       2 <int [20]>
##  3       3 <int [6]> 
##  4       4 <int [6]> 
##  5       5 <int [5]> 
##  6       7 <int [0]> 
##  7       8 <int [1]> 
##  8       9 <int [20]>
##  9      10 <int [20]>
## 10      11 <int [20]>
## # ... with 2,790 more rows

各銘柄に何のフレーバータグが付いているかを教えてくれる.

試しに複雑な味わいが印象的な二兎というお酒のフレーバータグを確認してみよう.

df_flavortags %>% 
  unnest(cols = everything()) %>% 
  left_join(df_brands, by="brandId") %>% 
  left_join(df_tags, by="tagIds") %>% 
  filter(brandname=="二兎")
## # A tibble: 20 x 5
##    brandId tagIds brandname breweryId tag       
##      <int>  <int> <chr>         <int> <chr>     
##  1    4195     80 二兎            987 ガス      
##  2    4195      2 二兎            987 酸味      
##  3    4195     20 二兎            987 バランス  
##  4    4195      5 二兎            987 旨味      
##  5    4195     17 二兎            987 苦味      
##  6    4195     12 二兎            987 甘味      
##  7    4195     90 二兎            987 複雑      
##  8    4195      6 二兎            987 フルーティ
##  9    4195     48 二兎            987 フレッシュ
## 10    4195     67 二兎            987 ジューシー
## 11    4195      9 二兎            987 しっかり  
## 12    4195     24 二兎            987 綺麗      
## 13    4195     42 二兎            987 メロン    
## 14    4195     26 二兎            987 華やか    
## 15    4195    100 二兎            987 さわやか  
## 16    4195     38 二兎            987 余韻      
## 17    4195      7 二兎            987 スッキリ  
## 18    4195     23 二兎            987 キレ      
## 19    4195     34 二兎            987 濃厚      
## 20    4195    126 二兎            987 軽快

同じ銘柄といっても, その銘柄の中での種類によって味わいが違うとはいえ, 二兎が目指している相反する性質の共存がよく表されているように思う.

 

今回のデータの構造を整理すると, こんな感じの構造になる. 

 

【学習予定】

以上で, さけのわデータの構造や扱い方が理解できたので, 次回の記事で, 日本酒の酸味に関する分類に挑戦してみる(やり方はまだ考え中で, 上手く行くかは不明).