Skip to main content

部屋の階数は家賃にどれだけ影響を与えるのか?

はじめに

昨年の確率的プログラミング言語アドベントカレンダーに出した記事(階層ベイズで東京23区のお部屋の家賃相場を推定する - suzuna’s memo)の続きです。(本記事を読む上では、この記事は読まなくて大丈夫です)

前の記事では、まず賃貸物件の情報サイトであるSUUMOをスクレイピングすることで、約20万件の東京23区の賃貸物件の家賃データを収集しました。そのデータを用いて、東京23区の家賃相場を推定する階層ベイズモデルをR + RStanで実装しました。なお、ここでいう家賃とは、毎月発生する家賃と管理費の合計を指します(以下、単に家賃と記載します)。

このモデルの説明変数には最寄り駅、部屋の面積、築年数、駅からの徒歩分数を使用しました。そのため、部屋がある階数の情報を考慮できていませんでした。同じマンションでも部屋の階数が高いほど家賃は高くなります。1階や地下1階の部屋の家賃は安く設定されていることが多いですし、2階と10階では家賃にそれなりの開きがあります。また、最寄り駅によって、高層マンションの物件が多い駅と低層マンションの物件が多い駅があるため、階を考慮しなければ、前者の駅ほど割高に推定することになります。

そこでこの記事では、先の東京23区の家賃相場のモデルの説明変数に部屋の階数を追加しました。これにより、階数が1階上がるごとに家賃がどの程度高くなるのかや、1階や地下階だとどの程度安くなるのかを解き明かしてみようと思います。また、築年数1年や、駅からの徒歩分数1分ごとに家賃がどの程度変化するかも合わせて示します。

さらに、最寄り駅以外の全ての条件(説明変数)を揃えたとき、最寄り駅によってどの程度家賃相場が変わるかを見てみようと思います。

結論

  • 築年数が1年増えるごとに家賃は1%下がる
  • 駅から徒歩1分増えるごとに家賃は0.7%下がる
  • 物件の階数が2階から1階上がるごとに家賃は1.2%上がる
  • 最上階かどうかは家賃に影響がない
  • 1階は2階と比べて家賃は3.6%、地下1階は2階と比べて家賃は6.6%下がる
  • 任意の最寄り駅、面積、築年数、駅からの徒歩分数、階数における物件の家賃相場を示すことができた
    • 例: 最寄り駅が表参道の25m2、築5年、駅から徒歩5分、3階のマンションの家賃相場は15.9万円

環境

  • R 4.3.1
  • rstan 2.32.3
  • bayesplot 1.10.0
  • tidybayes 3.0.6

使うデータ

2023年11月にSUUMOからスクレイピングした東京23区の賃貸マンションの賃貸データを用います。

用いたのは、東京23区の賃貸物件のうち、以下に該当する物件です。124354件の物件です。

  • SUUMOのカテゴリが「賃貸マンション」の物件(アパートや一戸建てを除く)1
  • 面積が15m2~100m2の物件2
  • マンションの高さが、地下階はないか地下1階まで、かつ地上階は15階以下の物件3
  • 築年数が40年以下の物件
  • 駅から徒歩(=車やバスではない)かつ駅からの徒歩分数が20分以内の物件
  • 家賃+管理費が100万円以下の物件
  • 階数の情報がページに存在し、かつ建物の地下階の階数<=物件の階数<=地上階の階数である物件
    • SUUMOの誤記入なのか、3階建てのマンションなのに部屋が4階と書かれているようなことがまれにあるが、そういうものを除くということ

25m2の物件と150m2の物件だったり、駅から徒歩10分の物件と駅からバスで20分、バス停から徒歩5分の物件を同じ線形モデルで説明することは無理があります。150m2の物件やバスで20分に徒歩5分の物件は、数としては多くないので、使用しないことにしたということです。

前処理の内容や可視化については前の記事で詳細に触れていますので興味があれば見てみてください。

モデル

家賃相場は、物件の最寄り駅、面積、築年数、駅からの徒歩分数、部屋の階数で決まると考えます。ここでいう家賃相場とは、これらの条件(=説明変数)なら平均的にはこのくらいの家賃になるという水準です(そのため、実際に観測される家賃は、この家賃相場の上下に分布します)。

具体的には、以下のモデルで定式化します。

物件$i(1, \dots, N)$の最寄り駅(SUUMOの物件ページで一番上に書いてある1番目の最寄り駅)を$sta[i] (1, \dots, S)$とします。このとき、物件の対数家賃の相場は$\mu_{i}$万円であると考えます。

$$ \begin{align*} \log{y_{i}} & \sim N(\mu_{i}, \sigma) \\\ \mu_{i} &= a_{sta[i]} + b_{sta[i]} \log{\mathrm{area}_{i}} \\\ &+ \beta_{\mathrm{age}} \mathrm{age}_{i} + \beta_{\mathrm{walk}}(\mathrm{walk}_{i} - 1) \\\ &+ \beta_{\mathrm{floor}} \max {(\mathrm{floor}_{i} - 2, 0)} \\\ &+ \beta_{\mathrm{isTop}} \mathrm{isTop}_{i} \\\ &+ \beta_{\mathrm{isGround}} \mathrm{isGround}_{i} \\\ &+ \beta_{\mathrm{isUnderground}} \mathrm{isUnderground}_{i} \\\ a_{sta[i]} & \sim N(a_{all}, \sigma_{a_{all}}) \\\ b_{sta[i]} & \sim N(b_{all}, \sigma_{b_{all}}) \\\ \end{align*} $$

ただし、物件$i$について、それぞれ以下の通りとします。

  • $y_{i}$: 家賃+管理費(万円)
  • $\mathrm{area}_{i} (15 \leq \mathrm{area}_{i} \leq 100)$: 面積(m2)
  • $\mathrm{age}_{i} (= 0, 1, \dots, 40)$: 築年数。新築は0年とする
  • $\mathrm{walk}_{i} (= 1, 2, \dots, 20)$: 最寄り駅からの徒歩分数
  • $\mathrm{floor}_{i} (= -1, 1, 2, \dots, 15)$: 物件の階数
  • $\mathrm{isTop}_{i} (= 0, 1)$: その部屋が最上階なら1, そうではないなら0
  • $\mathrm{isGround}_{i} (= 0, 1)$: その部屋が1階なら1, そうではないなら0
  • $\mathrm{isUnderground}_{i} (= 0, 1)$: その部屋が地下1階なら1, そうではないなら0

面積の対数と家賃の対数は線形の関係で、その切片と傾きは最寄り駅によって違うというモデルです。要するに最寄り駅によって家賃水準が変わってくるということです。同じ面積の部屋でも家賃の高い駅と安い駅がありますし、面積を大きくしたときに家賃の上がり幅が大きい駅と小さい駅があります。これを階層ベイズで表現します4

築年数が1年増えたり、最寄り駅から徒歩1分増えたり、部屋の階数が1階上がったりするごとに家賃が一定割合増減するという仮定を置きます。部屋が最上階なら追加で一定割合家賃が上がり、反対に1階や地下1階なら2階と比べて一定割合家賃が下がるとします(家探しをしたことがあれば何となく分かると思いますが、2階以上では階が上がるごとに一定割合家賃が上がっていく一方で、1階と地下1階はそれより大きい割合で家賃が安くなると思われるため、1階と地下1階は別のパラメータに分けました。ドメイン知識ですね)。これらの割合は、最寄り駅によらず一定とします。

なお、SUUMOでは物件ごとに最寄り駅が最大3つ書かれていますが、このモデルでは最初に書かれている1番目の最寄り駅のみを使用しています。他の最寄り駅も考慮するとより精緻になりそうですが、これでも駅ごとの家賃の大まかな傾向はとらえられると思われます。

Stanの実装

このモデルをStanのコードで書きます。事前分布は無情報事前分布です。

data {
  int N; // 物件の数
  vector[N] Y; // 家賃+管理費
  vector[N] AREA; // 面積
  int S; // 最寄り駅の数
  int<lower=1, upper=S> STATION[N]; // 物件nの最寄り駅index
  vector[N] AGE; // 物件nの築年数
  vector[N] WALK; // 物件nの徒歩分数
  vector[N] FLOOR; // 物件nの階数(ただし、1階や地下1階の場合は0)
  vector[N] IS_TOP;
  vector[N] IS_GROUND;
  vector[N] IS_UNDERGROUND;
}

parameters {
  real a0; // 面積の切片の全体平均
  real b0; // 面積の傾きの全体平均
  vector[S] a;
  vector[S] b;

  real<upper=0> age_b;
  real<upper=0> walk_b;

  real<lower=0> floor_b;
  real<lower=0> floor_b_is_top;
  real<upper=0> floor_b_is_ground;
  real<upper=0> floor_b_is_underground;

  real<lower=0> sigma_a;
  real<lower=0> sigma_b;
  real<lower=0> sigma;
}

model {
  a ~ normal(a0, sigma_a);
  b ~ normal(b0, sigma_b);

  log(Y) ~ normal(
    a[STATION] + b[STATION] .* log(AREA) +
    age_b * AGE +
    walk_b * (WALK - 1)+
    floor_b * (FLOOR - 2)+
    floor_b_is_top * IS_TOP +
    floor_b_is_ground * IS_GROUND +
    floor_b_is_underground * IS_UNDERGROUND,
    sigma
  );
}

このStanコードをmodel.stanというファイル名で保存し、以下のコードでRStanでキックします。chains=4, iter=5000, warmup=1000で約20時間かかりました。

library(tidyverse)
library(rstan)
library(bayesplot)
library(tidybayes)
library(patchwork)
# 上はMCMCの並列化、下はstanコードが変わらない限り再コンパイルしない
options(mc.cores=parallel::detectCores())
rstan::rstan_options(auto_write=TRUE)

# Stanコードのコンパイル
mod <- rstan::stan_model("model.stan")
# MCMCの実行
# dataはstanコードのdataブロックのN, Y, ...をlist(N=hoge, Y=fuga, ...)のように持つ
fit <- rstan::sampling(
  mod,
  data=data,
  chains=4, iter=5000, warmup=1000, thin=1, refresh=10, seed=1234
)

MCMCが収束していることをトレースプロットやRhatなどでチェックしましたが、結果は割愛します。(具体的な実装は前回の記事をご参照ください)

結果

Stanのパラメータ推定結果です。(一部のパラメータのみ抜粋)

Code
print(
  fit,
  pars=c(
    "a0", "b0", "age_b", "walk_b",
    "floor_b", "floor_b_is_top", "floor_b_is_ground", "floor_b_is_underground",
    "sigma_a", "sigma_b", "sigma"
  ),
  digits_summary=3
)
#> Inference for Stan model: anon_model.
#> 4 chains, each with iter=5000; warmup=1000; thin=1; 
#> post-warmup draws per chain=4000, total post-warmup draws=16000.
#> 
#>                          mean se_mean    sd   2.5%    25%    50%    75%  97.5%
#> a0                     -0.116       0 0.011 -0.139 -0.124 -0.116 -0.108 -0.094
#> b0                      0.803       0 0.005  0.793  0.799  0.803  0.806  0.812
#> age_b                  -0.011       0 0.000 -0.011 -0.011 -0.011 -0.011 -0.010
#> walk_b                 -0.007       0 0.000 -0.007 -0.007 -0.007 -0.007 -0.007
#> floor_b                 0.012       0 0.000  0.012  0.012  0.012  0.012  0.012
#> floor_b_is_top          0.000       0 0.000  0.000  0.000  0.000  0.000  0.000
#> floor_b_is_ground      -0.036       0 0.001 -0.038 -0.037 -0.036 -0.036 -0.034
#> floor_b_is_underground -0.068       0 0.006 -0.080 -0.072 -0.068 -0.064 -0.056
#> sigma_a                 0.219       0 0.009  0.203  0.213  0.219  0.225  0.237
#> sigma_b                 0.099       0 0.004  0.092  0.097  0.099  0.101  0.106
#> sigma                   0.110       0 0.000  0.110  0.110  0.110  0.111  0.111
#>                        n_eff Rhat
#> a0                     23848    1
#> b0                     24227    1
#> age_b                  16923    1
#> walk_b                 33368    1
#> floor_b                33061    1
#> floor_b_is_top         23531    1
#> floor_b_is_ground      35017    1
#> floor_b_is_underground 34735    1
#> sigma_a                16979    1
#> sigma_b                25626    1
#> sigma                  16128    1
#> 
#> Samples were drawn using NUTS(diag_e) at Thu Apr  4 16:17:37 2024.
#> For each parameter, n_eff is a crude measure of effective sample size,
#> and Rhat is the potential scale reduction factor on split chains (at 
#> convergence, Rhat=1).

築年数効果

以下、点推定値としてmedianを採用します。$\beta_{\mathrm{age}}$ = -0.011でした。これは、築年数が1年増えるごとに、家賃の対数が0.011小さくなることを意味します。

築年数1年につき家賃の対数が0.011小さくなると言われてもよく分からないので、家賃が何%小さくなるのかが知りたいですね。これは、$\mathrm{age}_{i} = 0, \dots, 40$としたときの$\exp (\beta_{\mathrm{age}} \mathrm{age}_{i})$の事後中央値と95%ベイズ信用区間を求めればよいです。

medianは事後分布の中央値、upperとlowerは95%ベイズ信用区間の上限と下限です。

Code
age_b <- tidy_draws |>
  pull(age_b)

res_age <- 0:40 |>
  map_dfr(\(age) {
    samples <- exp(age_b * age)
    tibble::tibble(
      age=age,
      median=quantile(samples, 0.5),
      lower=quantile(samples, 0.025),
      upper=quantile(samples, 0.975)
    )
  })
# きりのいいageだけ表示する
res_age |>
  filter(age %in% c(0:5, seq(5, 40, 5))) |>
  print(n=15)
#> # A tibble: 13 × 4
#>      age median lower upper
#>    <int>  <dbl> <dbl> <dbl>
#>  1     0  1     1     1    
#>  2     1  0.990 0.989 0.990
#>  3     2  0.979 0.979 0.979
#>  4     3  0.969 0.969 0.969
#>  5     4  0.959 0.959 0.959
#>  6     5  0.949 0.948 0.949
#>  7    10  0.900 0.900 0.901
#>  8    15  0.854 0.853 0.855
#>  9    20  0.810 0.809 0.811
#> 10    25  0.769 0.768 0.770
#> 11    30  0.729 0.728 0.730
#> 12    35  0.692 0.691 0.693
#> 13    40  0.656 0.655 0.658

medianの列のとおり、築年数が1年増えるごとに家賃は1%下がります。覚えやすいですね。

例えば築20年の物件は、新築の物件と比較して19%(≒1-0.99^20)下がります。

徒歩分数効果

Code
walk_b <- fit |>
  tidybayes::spread_draws(walk_b) |>
  pull(walk_b)

res_walk <- 1:20 |>
  map_dfr(\(walk) {
    samples <- exp(walk_b * (walk - 1))
    tibble::tibble(
      walk=walk,
      median=quantile(samples, 0.5),
      lower=quantile(samples, 0.025),
      upper=quantile(samples, 0.975)
    )
  })
res_walk |>
  filter(walk %in% c(1, 2, 3, 5, 10, 15, 20)) |>
  print()
#> # A tibble: 7 × 4
#>    walk median lower upper
#>   <int>  <dbl> <dbl> <dbl>
#> 1     1  1     1     1    
#> 2     2  0.993 0.993 0.993
#> 3     3  0.986 0.986 0.986
#> 4     5  0.972 0.972 0.973
#> 5    10  0.939 0.937 0.941
#> 6    15  0.907 0.904 0.909
#> 7    20  0.876 0.872 0.879

同様に、駅から徒歩1分増えるごとに家賃は0.7%下がります。例えば駅から徒歩10分の物件は徒歩1分の物件と比べて6.1%(≒1-0.993^9)下がります。駅から遠くても家賃はあまり下がりませんね。

徒歩分数1分あたりの家賃の変化量は全ての駅で一定としていますが、地上駅やターミナル駅では駅に近すぎると電車や駅周辺の騒音の影響で家賃が下がりそうな気もします。

階数効果

Code
options(pillar.sigfig=4)
floor_b <- fit |>
  tidybayes::spread_draws(floor_b) |>
  pull(floor_b)

res_floor <- 2:15 |>
  map_dfr(\(floors) {
    samples <- exp(floor_b * max(floors - 2, 0))
    tibble::tibble(
      floor=floors,
      median=quantile(samples, 0.5),
      lower=quantile(samples, 0.025),
      upper=quantile(samples, 0.975)
    )
  })
res_floor |>
  print(digits=5)
#> # A tibble: 14 × 4
#>    floor median lower upper
#>    <int>  <dbl> <dbl> <dbl>
#>  1     2  1     1     1    
#>  2     3  1.012 1.012 1.012
#>  3     4  1.024 1.023 1.024
#>  4     5  1.036 1.035 1.037
#>  5     6  1.048 1.047 1.049
#>  6     7  1.061 1.059 1.062
#>  7     8  1.073 1.072 1.075
#>  8     9  1.086 1.084 1.088
#>  9    10  1.099 1.097 1.101
#> 10    11  1.112 1.109 1.115
#> 11    12  1.125 1.122 1.128
#> 12    13  1.139 1.135 1.142
#> 13    14  1.152 1.148 1.156
#> 14    15  1.166 1.162 1.170

物件の階数が2階から上に1階上がるごとに家賃が1.2%上がることが分かりました。

最上階効果、1階効果、地下1階効果

Code
fit |>
  tidybayes::spread_draws(
    floor_b_is_top, floor_b_is_ground, floor_b_is_underground
  ) |>
  mutate(
    exp_is_top=exp(floor_b_is_top),
    exp_is_ground=exp(floor_b_is_ground),
    exp_is_underground=exp(floor_b_is_underground)
  ) |>
  tidybayes::median_qi(
    exp_is_top, exp_is_ground, exp_is_underground, .width=0.95
  )
#>   exp_is_top exp_is_top.lower exp_is_top.upper exp_is_ground
#> 1   1.000026         1.000001         1.000139     0.9644208
#>   exp_is_ground.lower exp_is_ground.upper exp_is_underground
#> 1           0.9625942           0.9662279          0.9338659
#>   exp_is_underground.lower exp_is_underground.upper .width .point .interval
#> 1                0.9228108                 0.945169   0.95 median        qi

以下のことが分かります。

  • 最上階であることは、家賃を全く押し上げない。(exp_is_top)
  • 1階の物件は、2階の物件と比べて家賃が3.6%下がる。(exp_is_ground)
  • 地下1階の物件は、2階の物件と比べて家賃が6.6%下がる5(exp_is_underground)

階数効果と合わせて考えると、例えば地上4階地下1階建てのマンションで2階が家賃10万円なら、3階は10.12万円、4階は10.24万円、1階は9.64万円、地下1階は9.34万円くらいになるということです。だいぶ妥当な感じの結果ですね。

2階から3階は1.2%上がる一方、2階から1階は3.6%、2階から地下1階は6.6%下がることから、やはり1階や地下1階の物件の家賃はディスカウントされているということが分かりました。

最上階だからといって、階数効果(1階につき1.2%)以上に追加で家賃を押し上げることはないというのがちょっと意外でした。ですが、新築のマンションの各部屋の家賃(新築は完成時に全階の物件が一斉に募集がかかる)を見たことがあるのですが、確かに最上階だからといって家賃が高くなることはないような気もしました。

最寄り駅効果

最寄り駅以外の条件を固定して、最寄り駅ごとに家賃相場がどの程度異なるかを見ることができます。25m2、築5年、駅から徒歩5分、3階の賃貸マンションという条件で、最寄り駅だけ変えてみましょう6。ちなみに25m2というのは一人暮らし用の物件でよくみられる面積です。

まずは京王線沿いの各駅です。

Code
# 駅名とモデルに投入したindexのマッピング
sta_chr_idx_table <- df_mod |>
  select(moyorieki_1_station, moyorieki_1_station_index) |>
  distinct(moyorieki_1_station, .keep_all=TRUE)

# 駅名があればそのindex, なければNA_integer_を返す
station_to_idx <- function(station_name) {
  chr <- sta_chr_idx_table$moyorieki_1_station
  idx <- sta_chr_idx_table$moyorieki_1_station_index
  if (length(idx[which(chr==station_name)]) == 0) {
    return(NA_integer_)
  } else {
    return(idx[which(chr==station_name)])
  }
}

tidy_draws_by_idx <- tidybayes::spread_draws(fit, a[idx], b[idx], age_b, walk_b, floor_b, floor_b_is_top, floor_b_is_ground, floor_b_is_underground, sigma_a, sigma_b)

stations <- c(
  "新宿駅", "初台駅", "幡ヶ谷駅", "笹塚駅", "代田橋駅", "明大前駅", "下高井戸駅", "桜上水駅", "上北沢駅", "八幡山駅", "芦花公園駅", "千歳烏山駅"
)
# factor型で駅の路線順に並べる
stations_fct <- forcats::fct_relevel(as.factor(stations), stations)
# 見る駅名のindex(stanのa[s]やb[s]のs)
idxs <- map_int(stations, station_to_idx)

area <- 25
age <- 5
walk <- 5
floor <- 3
is_top <- 0

p1 <- tidy_draws_by_idx |>
  filter(idx %in% idxs) |>
  # 駅のindexではなく駅名をプロットに付けるためにindexと駅名のテーブルをjoinする
  left_join(
    df_mod |>
      filter(moyorieki_1_station %in% stations) |>
      distinct(moyorieki_1_station, .keep_all=TRUE) |>
      select(moyorieki_1_station, moyorieki_1_station_index) |>
      rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |>
      mutate(station=forcats::fct_relevel(station, stations)),
    by="idx"
  ) |>
  mutate(
    mu_exp=exp(
      a+b*log(area)+age_b*age+walk_b*(walk-1)+
      floor_b*max(floor-2, 0)+
      floor_b_is_top*is_top+
      floor_b_is_ground*as.integer(floor == 1L)+
      floor_b_is_underground*as.integer(floor == -1L)
    )
  ) |>
  ggplot(aes(mu_exp, station))+
  theme_light()+
  tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
  scale_x_continuous(breaks=0:20)+
  theme(axis.title.y=element_blank())+
  labs(
    title="exp(mu_i) (25m2, 築5年, 徒歩5分, 3階)",
    subtitle="point: estimated (median), bar: 95% bayesian CI",
    x="exp(mu_i) (万円)",
    y="station"
  )
p2 <- df_mod |>
  filter(moyorieki_1_station %in% stations) |>
  count(moyorieki_1_station, moyorieki_1_station_index, name="n") |>
  rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |>
  mutate(station=forcats::fct_relevel(station, stations)) |>
  arrange(station) |>
  ggplot(aes(station, n))+
  theme_light()+
  geom_bar(stat="identity", color="black", fill="gray", alpha=0.6)+
  scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
  geom_text(aes(label=n, y=100))+
  theme(axis.title.y=element_blank())+
  coord_flip()+
  labs(
    title="(参考)物件数"
  )

patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))

左のプロットは最寄り駅と上の条件での家賃相場(万円)です。真ん中の点が推定値、左右の棒は95%ベイズ信用区間です。右のプロットはSUUMOにその最寄り駅の物件が何件あったかを示します。例えば、25m2、築5年、駅から徒歩5分、3階のマンションの家賃相場は、最寄り駅が新宿だと14.3万円、初台だと12.1万円ということを示します。ちなみに、例えば築10年だとこの結果に0.95(≒1-0.99^(10-5))をかけたものになります。

明大前が下高井戸と代田橋より少し高く、また千歳烏山が芦花公園より少し高いことが面白いですね。明大前と千歳烏山は特急~各駅の全ての列車が止まること、明大前は京王井の頭線(渋谷~吉祥寺)も通ることが理由でしょうか。桜上水は新宿まで10分と近く、特急以外が止まります。閑静で住みやすい街ですが比較的お手頃な家賃で、住むにはよさそうですね。

次に同じ条件で小田急線沿いを見てみます。京王線と同じく新宿が始発で、京王線の南側を走る路線です。

Code
stations <- c(
  "新宿駅", "南新宿駅", "参宮橋駅", "代々木八幡駅", "代々木上原駅", "東北沢駅", "下北沢駅", "世田谷代田駅", "梅ヶ丘駅", "豪徳寺駅", "経堂駅", "千歳船橋駅", "祖師ヶ谷大蔵駅", "成城学園前駅"
)
# factor型で駅の路線順に並べる
stations_fct <- forcats::fct_relevel(as.factor(stations), stations)
# 見る駅名のindex(stanのa[s]やb[s]のs)
idxs <- map_int(stations, station_to_idx)

area <- 25
age <- 5
walk <- 5
floor <- 3
is_top <- 0

p1 <- tidy_draws_by_idx |>
  filter(idx %in% idxs) |>
  # 駅のindexではなく駅名をプロットに付けるためにindexと駅名のテーブルをjoinする
  left_join(
    df_mod |>
      filter(moyorieki_1_station %in% stations) |>
      distinct(moyorieki_1_station, .keep_all=TRUE) |>
      select(moyorieki_1_station, moyorieki_1_station_index) |>
      rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |>
      mutate(station=forcats::fct_relevel(station, stations)),
    by="idx"
  ) |>
  mutate(
    mu_exp=exp(
      a+b*log(area)+age_b*age+walk_b*(walk-1)+
      floor_b*max(floor-2, 0)+
      floor_b_is_top*is_top+
      floor_b_is_ground*as.integer(floor == 1L)+
      floor_b_is_underground*as.integer(floor == -1L)
    )
  ) |>
  ggplot(aes(mu_exp, station))+
  theme_light()+
  tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
  scale_x_continuous(breaks=0:20)+
  theme(axis.title.y=element_blank())+
  labs(
    title="exp(mu_i) (25m2, 築5年, 徒歩5分, 3階)",
    subtitle="point: estimated (median), bar: 95% bayesian CI",
    x="exp(mu_i) (万円)",
    y="station"
  )
p2 <- df_mod |>
  filter(moyorieki_1_station %in% stations) |>
  count(moyorieki_1_station, moyorieki_1_station_index, name="n") |>
  rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |>
  mutate(station=forcats::fct_relevel(station, stations)) |>
  arrange(station) |>
  ggplot(aes(station, n))+
  theme_light()+
  geom_bar(stat="identity", color="black", fill="gray", alpha=0.6)+
  scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
  geom_text(aes(label=n, y=100))+
  theme(axis.title.y=element_blank())+
  coord_flip()+
  labs(
    title="(参考)物件数"
  )

patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))

代々木上原まで、下北沢まで、成城学園前までで分かれていますね。経堂~梅ヶ丘も桜上水と同じく新宿から10分強ですし、静かな住みやすい街でよいのではないでしょうか。経堂は快速急行以外が止まるので便利ですね。

最後に東京メトロの千代田線沿い(代々木上原~赤坂)を見てみます。国会議事堂前~大手町は物件がほとんどないので省略します。

Code
stations <- c(
  "代々木上原駅", "代々木公園駅", "明治神宮前駅", "表参道駅", "乃木坂駅", "赤坂駅"
)
# factor型で駅の路線順に並べる
stations_fct <- forcats::fct_relevel(as.factor(stations), stations)
# 見る駅名のindex(stanのa[s]やb[s]のs)
idxs <- map_int(stations, station_to_idx)

area <- 25
age <- 5
walk <- 5
floor <- 3
is_top <- 0

p1 <- tidy_draws_by_idx |>
  filter(idx %in% idxs) |>
  # 駅のindexではなく駅名をプロットに付けるためにindexと駅名のテーブルをjoinする
  left_join(
    df_mod |>
      filter(moyorieki_1_station %in% stations) |>
      distinct(moyorieki_1_station, .keep_all=TRUE) |>
      select(moyorieki_1_station, moyorieki_1_station_index) |>
      rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |>
      mutate(station=forcats::fct_relevel(station, stations)),
    by="idx"
  ) |>
  mutate(
    mu_exp=exp(
      a+b*log(area)+age_b*age+walk_b*(walk-1)+
      floor_b*max(floor-2, 0)+
      floor_b_is_top*is_top+
      floor_b_is_ground*as.integer(floor == 1L)+
      floor_b_is_underground*as.integer(floor == -1L)
    )
  ) |>
  ggplot(aes(mu_exp, station))+
  theme_light()+
  tidybayes::stat_pointinterval(point_interval=tidybayes::median_qi, .width=0.95)+
  scale_x_continuous(breaks=0:20)+
  theme(axis.title.y=element_blank())+
  labs(
    title="exp(mu_i) (25m2, 築5年, 徒歩5分, 3階)",
    subtitle="point: estimated (median), bar: 95% bayesian CI",
    x="exp(mu_i) (万円)",
    y="station"
  )
p2 <- df_mod |>
  filter(moyorieki_1_station %in% stations) |>
  count(moyorieki_1_station, moyorieki_1_station_index, name="n") |>
  rename(station=moyorieki_1_station, idx=moyorieki_1_station_index) |>
  mutate(station=forcats::fct_relevel(station, stations)) |>
  arrange(station) |>
  ggplot(aes(station, n))+
  theme_light()+
  geom_bar(stat="identity", color="black", fill="gray", alpha=0.6)+
  scale_y_continuous(breaks=seq(0, 2000, 500), minor_breaks=seq(0, 2000, 100))+
  geom_text(aes(label=n, y=100))+
  theme(axis.title.y=element_blank())+
  coord_flip()+
  labs(
    title="(参考)物件数"
  )

patchwork::wrap_plots(p1, p2, ncol=2, widths=c(3, 2))

赤坂は15.1万円、表参道は15.9万円、明治神宮前は16.8万円!高いですね…。他の駅なら同じ金額で二人暮らし用の物件が借りられますね。この辺りはどこも高く、例えば東京メトロ銀座線の外苑前は15.9万円、東急東横線の代官山は15.2万円です。

おわりに

部屋の階数や最寄り駅などによってどの程度家賃相場が変わるのかを定量的に示すことができ、役に立ちそうな結果が得られました。

ツリー系の機械学習モデルの方が家賃の予測精度は高そうですが、1階は2階と比べて3.6%安いとか、初台~笹塚はほとんど家賃が変わらないといった解釈に使える知見を得るという点では統計モデリングが強いですね。ベイズモデリングなので、築年数効果のような各パラメータや家賃相場の幅をベイズ信用区間という形で知ることができるのもいい点です。

家賃はまさに階層ベイズ向きのテーマで面白いですね。今後もモデルをブラッシュアップしていきたいです。


  1. アパートは木造が多くマンションは鉄筋コンクリートが多いですが、木造と鉄筋コンクリートでは耐用年数が異なるため築年数が経過することによる家賃の押し下げ効果が異なると思われます。またアパートは高くても3階程度までですがマンションはより高く建てられることから、部屋の階数による家賃への影響もアパートとマンションで異なりそうです。そのため、この記事では賃貸マンションのみに絞りました。 ↩︎

  2. 前の記事では10m2~100m2としていましたが、10m2近辺の物件でモデルの当てはまりが悪いことが分かっています。つまり10m2近辺では面積と家賃の間の関係性が崩れていると思われます。本記事では15m2以上に引き上げました。 ↩︎

  3. 地上階の高さが高すぎるマンションや地下階が深すぎるマンションは東京23区の賃貸物件ではわずかなため、階数が家賃に与える効果をロバストに推定する観点からこの条件を加えました。なお、15階以下としているのは、16階以上のマンションはごくわずかであるためです(前回の記事をご参照)。 ↩︎

  4. この階層ベイズモデルでは、各最寄り駅における切片と傾きは東京23区全体のそれら(=23区の平均値)から一定程度ばらついたものであると定式化しています。これは、地価を考慮するとデータ生成のメカニズムに沿っていて理にかなったものです。また、最寄り駅ごとに別々に線形回帰するのではなく東京23区全体の傾向を借用することで、データ数が少ない最寄り駅の物件でもパラメータの推定が行えるのも階層ベイズのメリットです(縮約といいます)。 ↩︎

  5. 地下1階効果の95%ベイズ信用区間は5.5%-7.7%と若干広いです。これはパラメータ推定に使った物件データの中に地下1階の物件の数があまり多くなかったからです。 ↩︎

  6. 他に最上階ではないという条件も与えていますが、これまで見たように最上階かどうかは家賃に影響を与えないので、最上階だとしてもプロットは変わりません。 ↩︎