15  TIMSSの分析

intsvyを使えば,TIMSSもPISAと同じように分析することが可能です。 ただ,いくつか違う点もあります。

15.1 ファイル名

TIMSSのデータファイルは,国ごとに分割され,命名規則に従ってファイル名が付けられています。 分析者にとって重要になるのは,主として以下のファイルです。 ここでは第4学年(ファイル名がAで始まる)のファイルを例に説明します。 第8学年のファイルは,ファイル名がBで始まることと, BTM(数学の教員質問)・BTS(理科の教員質問)の二種類が存在する点が違います。

  • ACG: 第4学年・学校質問(school context data)
  • ASG: 第4学年・生徒質問(student context data)
  • ASH: 第4学年・家庭質問(home context data)
  • AST: 第4学年・生徒教員リンク(student-teacher linkage)
  • ATG: 第4学年・教員質問(teacher context data)

15.2 intsvyによるTIMSSの分析

ここでは最新のTIMSS2019を例に,intsvyによる分析方法を示します。 最初にデータを読み込みます。 TIMSSのデータファイルはSPSSというソフトウェア向けのファイルフォーマットで 公開されているので,Rのforeignというパッケージにある read.spss関数を使って読み込みます。 後にintsvyを使う関係上,read.spss関数の設定は, 以下のようにしてください。

url_stu <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2019/asgjpnm7.sav"
jpn09g4stu <- foreign::read.spss(url_stu,
  use.value.labels = FALSE,
  to.data.frame = TRUE, use.missings = FALSE
)

まず児童に尋ねた家庭のある本の冊数(ASBG04変数)の回答と, 男女(ITSEX変数)の人数を確認してみましょう。 いずれの処理も,timss.table関数で実行できます。 出力は,pisa.tableの場合と同じです。

# 家庭にある本の冊数
intsvy::timss.table("ASBG04", data = jpn09g4stu)
  ASBG04 Freq Percentage Std.err.
1      1  595      14.51     0.72
2      2 1220      29.55     0.81
3      3 1545      36.63     0.86
4      4  521      12.12     0.60
5      5  295       6.88     0.42
9      9   14       0.31     0.09
# 性別
intsvy::timss.table("ITSEX", data = jpn09g4stu)
  ITSEX Freq Percentage Std.err.
1     1 2038      48.39     0.48
2     2 2158      51.61     0.48

続いて算数・理科の平均値を計算します。 timss.mean.pv関数を使うと,5つのPVを使い, 平均値の計算とJK法による標準誤差の算出まで行ってくれます。 pisa.mean.pv関数と同じく,引数byを指定することで男女(ITSEX)別や 本の冊数(ASBG04)別の平均値も計算してくれます。

# 算数の平均点
intsvy::timss.mean.pv(paste0("ASMMAT0", 1:5), data = jpn09g4stu)
  Freq   Mean s.e.    SD s.e
1 4196 592.96 1.75 70.21   1
# 理科の平均点
intsvy::timss.mean.pv(paste0("ASSSCI0", 1:5), data = jpn09g4stu)
  Freq   Mean s.e.    SD  s.e
1 4196 561.66 1.77 68.91 1.56
# 男女別
intsvy::timss.mean.pv(paste0("ASMMAT0", 1:5), by = "ITSEX", data = jpn09g4stu)
  ITSEX Freq   Mean s.e.    SD  s.e
1     1 2038 593.34 2.18 67.11 1.29
2     2 2158 592.59 1.94 72.99 1.31
# 家にある本の冊数別
intsvy::timss.mean.pv(paste0("ASSSCI0", 1:5), by = "ASBG04", data = jpn09g4stu)
  ASBG04 Freq   Mean  s.e.    SD   s.e
1      1  595 528.26  3.46 66.41  2.90
2      2 1220 547.75  2.78 66.93  1.96
3      3 1545 572.66  2.20 64.69  2.00
4      4  521 582.04  3.94 64.46  3.02
5      5  295 599.91  4.76 64.51  3.68
6      9   14 487.47 27.14 86.89 18.28
7   <NA>    6 597.36 18.86 81.25 30.20

15.3 学校票の結合

学校票を結合する方法もPISAとほぼ同様です。 以下では,学校票のデータをjpn09g4schに格納し,生徒票のデータとmerge関数で結合しています。

url_sch <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2019/acgjpnm7.sav"
jpn09g4sch <- foreign::read.spss(url_sch,
  use.value.labels = FALSE,
  to.data.frame = TRUE, use.missings = FALSE
)

jpn09g4v1 <- merge(jpn09g4stu, jpn09g4sch,
  by = c(
    "IDCNTRY", "IDSCHOOL", "IDPOP", "IDGRADER", "IDGRADE",
    "WGTADJ1", "WGTFAC1", "VERSION", "SCOPE"
  )
)

結合した後は,timss.table関数などを使って分析が可能です。 以下の例では,経済的に恵まれない家庭の児童の割合(ACBG03A変数)の回答について, 学校票の回答割合(table関数を使って度数分布のみ出力)と, timss.table関数を使った児童の割合を出力しています。

# ACBG03Aの回答(学校単位)
table(jpn09g4sch$ACBG03A)

 1  2  3  4 
77 56 13  1 
round(prop.table(table(jpn09g4sch$ACBG03A)) * 100, 1)

   1    2    3    4 
52.4 38.1  8.8  0.7 
# ACBG03Aの回答(児童単位)
intsvy::timss.table("ACBG03A", data = jpn09g4v1)
  ACBG03A Freq Percentage Std.err.
1       1 2207      51.69     4.17
2       2 1569      38.21     4.22
3       3  386       9.34     2.38
4       4   34       0.76     0.76

回答は「1: 0〜10%」「2: 11〜25%」「3: 26〜50%」「4: 50%より多い」の4択です。 度数分布を見ると,1が77校(52.4%)・2が56校(38.1%)・・・であることがわかります。 またtimss.tableの結果から,1の学校に所属する児童の割合が51.69%でもっとも多いことがわかります。

15.4 保護者票の結合

TIMSSでは保護者票を結合することも可能です。 ここでは先ほど学校票を結合したデータフレームjpn09g4v1にさらに保護者票を結合します。

url_par <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2019/ashjpnm7.sav"
jpn09g4home <- foreign::read.spss(url_par,
  use.value.labels = FALSE,
  to.data.frame = TRUE, use.missings = FALSE
)
jpn09g4v2 <- merge(jpn09g4v1, jpn09g4home,
  by = c(
    "IDCNTRY", "IDSCHOOL", "IDCLASS", "IDSTUD", "IDPOP", "IDGRADER",
    "IDGRADE", "ASDAGE", "ASBGHRL", "ASDGHRL", "VERSION", "SCOPE"
  )
)

母親の学歴(ASBH15B変数)の回答割合に注目してみましょう。 ここでは回答者の母親学歴の分布(table関数で出力), 母親学歴ごとの児童の分布(timss.table関数で出力), 母親学歴ごとの児童の数学の平均値(timss.mean.pv関数で出力)のそれぞれを出力します。

# 母親の学歴
table(jpn09g4home$ASBH15B)

   3    4    5    6    7    8    9   99 
 149 1205  108 1672  821   47    7   76 
intsvy::timss.table("ASBH15B", data = jpn09g4v2)
   ASBH15B Freq Percentage Std.err.
3        3  149       3.63     0.31
4        4 1205      29.97     0.92
5        5  108       2.67     0.24
6        6 1672      40.85     0.79
7        7  821      19.74     0.90
8        8   47       1.12     0.22
9        9    7       0.16     0.06
99      99   76       1.85     0.23
intsvy::timss.mean.pv(paste0("ASMMAT0", 1:5), by = "ASBH15B", data = jpn09g4v2)
  ASBH15B Freq   Mean  s.e.    SD   s.e
1       3  149 540.60  5.31 62.61  4.45
2       4 1205 575.01  2.16 66.46  1.95
3       5  108 579.11  5.71 57.77  4.26
4       6 1672 599.69  2.31 67.40  1.52
5       7  821 625.30  2.92 65.49  2.09
6       8   47 633.01  9.00 62.05  7.51
7       9    7 548.29 36.66 93.37 32.29
8      99   76 561.35 11.55 74.87  8.18
9    <NA>  111 546.08  9.21 65.53  5.74

母親の学歴の回答は, 「1:学校に行っていない」 「2:小学校」 「3:中学校」 「4:高等学校」 「5:高等学校の専攻科」 「6:短期大学,高等専門学校,専門学校」 「7:大学」 「8:大学院」 「9:あてはまらない」 の9択です。 回答者の人数を見ると, 6(短期大学,高等専門学校,専門学校)が1672人でもっとも多く, ついで4(高等学校)が1205人です。 児童の割合に注目すると,6(短期大学,高等専門学校,専門学校)の母親のいる児童が 全体の40.85%ということになります。 数学の成績に着目すると,基本的に母親の学歴が高いほど(=回答番号が3から8になるにつれて) 児童の成績が高くなることがわかります。 ただし,8(大学院)は保護者の数が少ないこともあって標準誤差(s.e.)が9.00と大きくなっています。

15.5 教員票の結合

TIMSSでは,児童を教えている教員に対しても質問票が配布されています。 教員票を使う場合,ATGで始まる教員票に加え,ASTで始まるリンクファイルもダウンロードする必要があります。

なお,今回のように複数のデータフレームを結合する場合,merge関数ではどうしても書きにくくなります。 そこで,dplyrパッケージを使った結合を行っています。 dplyrはRの処理を円滑に行うために開発されたパッケージの一つです。 現代のRプログラミングでは必須とも言えるものなので, Rに関心のある方は学んでみるとよいでしょう。 dplyrを使った場合は,データフレーム間で共通する変数が自動的に認識されて結合されます。

以下では分析例として,教員の学歴(ATBG04変数)の度数分布を表示しています。

url_l1 <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2019/astjpnm7.sav"
url_l2 <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2019/atgjpnm7.sav"

jpn09g4link <- foreign::read.spss(url_l1,
  use.value.labels = FALSE,
  to.data.frame = TRUE, use.missings = FALSE
)
jpn09g4tea <- foreign::read.spss(url_l2,
  use.value.labels = FALSE,
  to.data.frame = TRUE, use.missings = FALSE
)

# dplyrを使うと簡単にマージ可能
suppressMessages(library(dplyr))
jpn09g4v3 <- jpn09g4stu |>
  inner_join(jpn09g4link) |>
  inner_join(jpn09g4tea)
Joining with `by = join_by(IDCNTRY, IDBOOK, IDSCHOOL, IDCLASS, IDSTUD, IDPOP,
IDGRADER, IDGRADE, JKREP, JKZONE, ASMMAT01, ASMMAT02, ASMMAT03, ASMMAT04,
ASMMAT05, ASSSCI01, ASSSCI02, ASSSCI03, ASSSCI04, ASSSCI05, ASMNUM01, ASMNUM02,
ASMNUM03, ASMNUM04, ASMNUM05, ASMGEO01, ASMGEO02, ASMGEO03, ASMGEO04, ASMGEO05,
ASMDAT01, ASMDAT02, ASMDAT03, ASMDAT04, ASMDAT05, ASMKNO01, ASMKNO02, ASMKNO03,
ASMKNO04, ASMKNO05, ASMAPP01, ASMAPP02, ASMAPP03, ASMAPP04, ASMAPP05, ASMREA01,
ASMREA02, ASMREA03, ASMREA04, ASMREA05, ASSLIF01, ASSLIF02, ASSLIF03, ASSLIF04,
ASSLIF05, ASSPHY01, ASSPHY02, ASSPHY03, ASSPHY04, ASSPHY05, ASSEAR01, ASSEAR02,
ASSEAR03, ASSEAR04, ASSEAR05, ASSKNO01, ASSKNO02, ASSKNO03, ASSKNO04, ASSKNO05,
ASSAPP01, ASSAPP02, ASSAPP03, ASSAPP04, ASSAPP05, ASSREA01, ASSREA02, ASSREA03,
ASSREA04, ASSREA05, ASSENV01, ASSENV02, ASSENV03, ASSENV04, ASSENV05, ASMIBM01,
ASMIBM02, ASMIBM03, ASMIBM04, ASMIBM05, ASSIBM01, ASSIBM02, ASSIBM03, ASSIBM04,
ASSIBM05, VERSION, SCOPE)`
Joining with `by = join_by(IDCNTRY, IDSCHOOL, IDPOP, IDGRADER, IDGRADE,
VERSION, SCOPE, IDTEALIN, IDTEACH, IDLINK, IDSUBJ, ITCOURSE)`
# ATBG04: 教員の学歴
table(jpn09g4tea$ATBG04)

  4   5   6 
 14 255  13 
round(prop.table(table(jpn09g4tea$ATBG04)) * 100, 1)

   4    5    6 
 5.0 90.4  4.6 

ほとんどの教員が「5: 大卒」であることがわかります。 「4: 短大卒」や「6: 大学院卒」もわずかにいます。

教員票を使った分析をする際に注意することは,標本ウェイトが変わるという点です。 TIMSSでは,児童を担当している教員はすべて教員票の対象になっています。 そのため算数や理科で複数の担任が担当していた場合, 児童1人に対して2名以上の教員票があるということも珍しくありません。 そのため教員票を結合すると,個々の教員に担当している児童が接続され, データセット内に同じ児童のデータが複数存在することになるのです。 このまま平均値などを計算すると,児童がダブルカウント(場合によってはトリプルカウント)されるため, 標本ウェイトを調整し,ダブルカウントの児童のウェイトは半分にするといった処理が行われています。 このウェイトをTIMSSでは教員ウェイト(TCHWGT)と呼んでいます。 intsvyでは,引数のtimss4_confを設定することでウェイトを変更可能です。 ただしこの場合は,timss.mean.pvではなく,intsvy.mean.pv関数を使う必要があります。 以下の例では,ウェイトを変更しなかった場合と変更した場合の推定値を比較しています。 度数(Freq)がjpn09g4v3では変わっていることも確認してください。

# 正しい推定値(児童票のみ)
intsvy::timss.mean.pv(paste0("ASMMAT0", 1:5), data = jpn09g4stu)
  Freq   Mean s.e.    SD s.e
1 4196 592.96 1.75 70.21   1
# 正しい推定値(学校票・保護者票を追加しても問題ない)
intsvy::timss.mean.pv(paste0("ASMMAT0", 1:5), data = jpn09g4v2)
  Freq   Mean s.e.    SD s.e
1 4196 592.96 1.75 70.21   1
# ウェイトを変更せずに教員票を含むデータセットを使うと間違った推定値が出力される
intsvy::timss.mean.pv(paste0("ASMMAT0", 1:5), data = jpn09g4v3)
  Freq  Mean s.e.   SD s.e
1 7946 593.3 1.88 70.5   1
# timss09g4_tchconfでウェイトを変更
timss09g4_tchconf <- intsvy::timss4_conf
timss09g4_tchconf$variables$weight <- "TCHWGT"
# ウェイトを変更すると適切な推定値が得られる
intsvy::intsvy.mean.pv(
  pvnames = paste0("ASMMAT0", 1:5),
  data = jpn09g4v3, config = timss09g4_tchconf
)
  Freq   Mean s.e.    SD s.e
1 7946 592.96 1.75 70.21   1
# 教員の学歴別に算数の平均値を計算
intsvy::intsvy.mean.pv(
  pvnames = paste0("ASMMAT0", 1:5), by = "ATBG04",
  data = jpn09g4v3, config = timss09g4_tchconf
)
  ATBG04 Freq   Mean s.e.    SD  s.e
1      4  353 591.46 5.65 65.12 2.96
2      5 7133 593.30 1.89 70.33 1.02
3      6  358 582.92 5.50 71.14 2.71
4   <NA>  102 626.24 3.01 60.08 7.04

15.6 第8学年のデータを扱う

第8学年のデータも第4学年のデータと同様に分析可能です。 違いはファイル名と,教員票が数学と理科のそれぞれに存在する点です。

url_g8s <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2019/bsgjpnm7.sav"
jpn09g8stu <- foreign::read.spss(
  url_g8s, to.data.frame = TRUE, use.value.labels = FALSE, use.missings = FALSE
)
intsvy::timss.mean.pv(paste0("BSMMAT0", 1:5), data = jpn09g8stu)
  Freq   Mean s.e.    SD  s.e
1 4446 594.23 2.72 84.14 2.06

以下の例では,第8学年の数学の平均値を教員の学歴(BTBG04変数)別に出力しています。 最初に,複数のデータファイルをlapply関数を使って一気にダウンロードしています。 その上でpurrrreduce関数を使って複数のデータフレームを結合し,intsvy.mean.pv関数で分析しています。 数学の教員ウェイトを使う必要があるので,標本ウェイトはMATWGTに変更しています。 出力を見ると,標準誤差(s.e.)がNaNになっている箇所がありますが, これは該当する教員が1名しかいないので標準誤差が計算できなかったためです。

# 複数のファイルを一気にダウンロード
url_g8 <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2019/b"
jpn09g8data <- lapply(c("sg", "st", "tm"), function(x) {
  foreign::read.spss(paste0(url_g8, x, "jpnm7.sav"),
    use.value.labels = FALSE,
    to.data.frame = TRUE, use.missings = FALSE
  )
})
# データフレームを結合
jpn09g8 <- jpn09g8data |>
  purrr::reduce(inner_join)
Joining with `by = join_by(IDCNTRY, IDBOOK, IDSCHOOL, IDCLASS, IDSTUD, IDPOP,
IDGRADER, IDGRADE, JKREP, JKZONE, BSMMAT01, BSMMAT02, BSMMAT03, BSMMAT04,
BSMMAT05, BSSSCI01, BSSSCI02, BSSSCI03, BSSSCI04, BSSSCI05, BSMALG01, BSMALG02,
BSMALG03, BSMALG04, BSMALG05, BSMAPP01, BSMAPP02, BSMAPP03, BSMAPP04, BSMAPP05,
BSMDAT01, BSMDAT02, BSMDAT03, BSMDAT04, BSMDAT05, BSMGEO01, BSMGEO02, BSMGEO03,
BSMGEO04, BSMGEO05, BSMKNO01, BSMKNO02, BSMKNO03, BSMKNO04, BSMKNO05, BSMNUM01,
BSMNUM02, BSMNUM03, BSMNUM04, BSMNUM05, BSMREA01, BSMREA02, BSMREA03, BSMREA04,
BSMREA05, BSSAPP01, BSSAPP02, BSSAPP03, BSSAPP04, BSSAPP05, BSSBIO01, BSSBIO02,
BSSBIO03, BSSBIO04, BSSBIO05, BSSCHE01, BSSCHE02, BSSCHE03, BSSCHE04, BSSCHE05,
BSSEAR01, BSSEAR02, BSSEAR03, BSSEAR04, BSSEAR05, BSSKNO01, BSSKNO02, BSSKNO03,
BSSKNO04, BSSKNO05, BSSPHY01, BSSPHY02, BSSPHY03, BSSPHY04, BSSPHY05, BSSREA01,
BSSREA02, BSSREA03, BSSREA04, BSSREA05, BSSENV01, BSSENV02, BSSENV03, BSSENV04,
BSSENV05, BSMIBM01, BSMIBM02, BSMIBM03, BSMIBM04, BSMIBM05, BSSIBM01, BSSIBM02,
BSSIBM03, BSSIBM04, BSSIBM05, VERSION, SCOPE)`
Joining with `by = join_by(IDCNTRY, IDSCHOOL, IDPOP, IDGRADER, IDGRADE,
VERSION, SCOPE, IDTEALIN, IDTEACH, IDLINK, IDSUBJ, ITCOURSE)`
# timss8_confを修正
timss09g8m_tchconf <- intsvy::timss8_conf
timss09g8m_tchconf$variables$weight <- "MATWGT"
# 教員の学歴(BTBG04)ごとの数学の平均値を出力
intsvy::intsvy.mean.pv(
  pvnames = paste0("BSMMAT0", 1:5), by = "BTBG04",
  data = jpn09g8, config = timss09g8m_tchconf
)
  BTBG04 Freq   Mean s.e.    SD   s.e
1      4   72 581.96 6.51 73.97 14.05
2      5 5136 595.35 3.15 85.08  2.13
3      6  834 588.82 4.93 77.29  3.12
4     99   36 568.53  NaN 88.02   NaN
5   <NA>   35 587.10  NaN 70.07   NaN
# SEがNAなのは該当する教員が1名しかいないため
table(jpn09g8data[[3]]$BTBG04, exclude = NULL)

   4    5    6   99 <NA> 
   2  179   27    1    1 
intsvy::intsvy.mean.pv(
  pvnames = paste0("BSMMAT0", 1:5),
  data = jpn09g8, config = timss09g8m_tchconf
)
  Freq   Mean s.e.    SD  s.e
1 6113 594.23 2.72 84.14 2.06

15.7 過去のTIMSS

TIMSS2019の分析方法は,過去のTIMSSにも適用可能です。 たとえばTIMSS2003で,第4学年の日本の平均値を出力する方法は,以下のようになります。

url_03 <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss2003/ASGJPNm3.sav"
jpn03g4stu <- foreign::read.spss(url_03,
  use.value.labels = FALSE,
  to.data.frame = TRUE, use.missings = FALSE
)
intsvy::timss.mean.pv(paste0("ASSSCI0", 1:5), data = jpn03g4stu)
  Freq   Mean s.e.    SD  s.e
1 4535 543.47 1.54 73.12 1.14

ただし,TIMSS1995だけは現在のTIMSSと調査設計が違うので注意が必要です。 SPSSファイルの場合,JRRの計算に必要なJKREP変数が小文字になっているので, そのままだとintsvyが動きません。 PVの変数名も小文字なので,引数もpaste0("asssci0", 1:5)になります。 さらにTIMSS1995では第3学年も調査対象になっていたので, 第4学年のデータを得るには引数byに学年(IDGRADE変数)を指定する必要もあります。

url_95 <- "https://raw.githubusercontent.com/kawa5902/LSAdata/refs/heads/main/timss1995/ASGJPNm1.sav"
jpn95stu <- foreign::read.spss(url_95,
  use.value.labels = FALSE,
  to.data.frame = TRUE, use.missings = FALSE
)
re-encoding from CP1252
# jkrepをJKREPに変更
names(jpn95stu)[names(jpn95stu) == "jkrep"] <- "JKREP"
# byにIDGRADEを指定し,学年ごとに結果を出力
intsvy::timss.mean.pv(paste0("asssci0", 1:5), by = "IDGRADE", data = jpn95stu)
  IDGRADE Freq   Mean s.e.    SD  s.e
1       3 4306 501.03 1.63 73.68 1.29
2       4 4306 553.18 1.74 72.50 1.21