Advent Calendar

千葉は素晴らしい旅行先だよ, という話(地価公示データを処理するコードもあるよ!)

⚠記事の内容は学生個人の見解であり、所属する学科組織を代表するものではありません。

はじめに

こんにちは, 経済物理学班の館長です. 聞くところによるとこの理物 Advent Calendar はノロケを語る場所だそうですので, 今日はサンキュー♡ちばフリーパスでともに過ごした千葉県との日々を存分にノロケていこうと思います. 経済物理要素どこだよ, と思われた方もいらっしゃるかもしれませんが, この記事で紹介するのは経済的な千葉旅行ですし, 物理の対象はそれこそ経済に至るまで自然界のすべてにわたりますから, 何もおかしくはない. いや地価公示データにしか興味はねえんだよ, という方はおまけまで飛んでください(ホントは悲しいけど…)

サンキュー♡ちばフリーパスって?

サンキュー♡ちばフリーパス(♡の密度が高いと筆者の心が疲れる1ので, 以下単にフリーパス)は, 千葉県とJR東日本とが連携して毎年ごろに発売しているおトクな切符です. 2022年は9/1~11/303ヶ月間が対象でした. 内容はシンプルで, 千葉県内のさまざまな鉄道やバスが2日間乗り放題というもの. 詳細は公式情報(例えば千葉県の告知ページなど)を参照していただきたいですが, 千葉県のほとんど全域をカバーしています. 鉄道だけでなくバスも乗り放題というのが本当にありがたく, 普段はあまり足を伸ばせないようなスポットも気軽に訪問することができるのは爆アドです. 3,970円(千葉県内から)または4,790円(東京都区内から)と, お手頃価格なのも嬉しいところ. 移動は安く抑えたい私たち学生にはピッタリですね.

千葉のココが好き!

フリーパスを知って「千葉旅行, アリかもな」と思ったあなたのために, 筆者の考える千葉の素敵ポイントをいくつか紹介します. いよいよ本編, ノロケパートですね. 場所のイメージがしやすいように, チーバくんの画像をお手元にご用意いただくのもオススメです.

好きなところその1: It's a small world!

千葉はなんと全国4位(令和2年時点)の農業生産額を誇る農業大国であり, 田畑の広がるのどかな景色を眺めることができます. みなさんも目を閉じて想像してみましょう. いわゆる日本の原風景というやつですね2. 一般的な田園風景 さて, ではここで千葉の田園風景(チーバくんの重心あたりにて撮影)をご覧ください. 千葉の田園風景 何か気づきませんか? そう, 千葉は空が広い! 千葉は大部分が房総半島であるわけですが, その地形の大きな特徴はなんといっても「平坦さ」です. 県内最高峰の愛宕山すらその標高は408mであり, これは全都道府県中最低となっています. そんな千葉ですから, 遠景に高い山がありません. 周囲を見渡したとき, 視界の端となるのは丘と呼んでもよいくらいの可愛い山ばかり. のどかな田園風景に小さな山々…ゆったりと眺めると, もしかして世界は案外小さいんじゃないか, いま見えている範囲ですべてなんじゃないかなんて感じがして, あくびの一つや二つも出てきます. 筆者が子供時代を過ごしたのは田畑などない東京の片隅ですが, 最高に心が安らぐ瞬間です.

好きなところその2: 素敵な灯台がある!

千葉はその周りを海に囲まれていますから, 多くの灯台を擁しています. いくつかは観光スポットとしても整備されており, 海辺好き・灯台好きの筆者としてはたまりません. 中でも筆者のお気に入りは, チーバくんの足裏3にある野島崎灯台です. 日本発の洋式灯台の1つで, 上から太平洋を一望することもでき…などと言っても始まりませんね, とりあえず筆者のベスト・ショットを貼っておきます. 野島崎灯台

うわ〜いい景色!ボンヤリとしたどこか不気味な緑の灯り, 周囲の荒凉とした風景, じわじわと暗くなっていく空…スティーブン・キングの表紙になっていたって違和感のない, なんだか不安になってくるような素敵な空間です. 参考までに, 点灯時間は11月半ばで17時ごろでした. みなさんも, ぜひ灯台の光を眺めてみてはいかがでしょうか4.

好きなところその3: 紅葉狩りもできる!

フリーパスの発売期間は秋ということで, せっかく旅行に行くのだったら紅葉が見たいな…という方もいらっしゃるかと思います. ご安心ください, 千葉には養老渓谷や成田山新勝寺をはじめ, 紅葉スポットも多数存在します5. 養老渓谷の紅葉 山はないと言いつつこういった風景はしっかり押さえているあたり, なんとも心憎いやつですね. 田園に海辺に森にと全方位に死角なしなんて, やっぱり千葉は最強の都道府県だな!

結論

フリーパスを買いましょう, そして千葉に行きましょう. 千葉で知っているスポットが某テーマパーク6だけ, というのはあまりに勿体ないと思います. 秋ごろにどこかにフラっと出かけたくなったとき, 「そういや千葉って手もあったな」と思っていただけるなら筆者は幸せ者です.

おまけ

Qiitaの記事削除対策ここまで読んでくださった皆さんへのお土産として, この前諸事情で作成した「地価公示データから座標, 価格, 面積を取り出してDataFrameにまとめるためのJuliaコード(Jupyter labで作成)」を載せておきます. より細かい地区や土地の用途も絞り込めます.

地価公示データの処理のためのコード
using DataFrames
using CSVFiles

#yyには「対象年の西暦下2桁」をstringで入れる(ここでは2003年)
yy = "03"
#Scopeには絞りたい地区名を正規表現で入れておく(ここでは中央区+港区)
Scope = r"中央区|港区"
    
AllCoordinates = []
InScopeCoordinates = []
AllPrices = []
InScopePrices = []
AllAcreages = []
InScopeAcreages = []
IsInScope = true
IsInScope2 = false
PointID = 0

if (parse(Int, yy) <= 11) || (parse(Int, yy) >= 83)
    f = open(string("L01-", yy, "_13-g.txt"), "r") #_13-g.txtの13のところは都道府県コード(ここでは東京), 以下同様
        Data = readlines(f)
    close(f)
    for line in Data
        if occursin("gml:pos", line) == true
            start = match(r">", line).offset + 1
            push!(AllCoordinates, [round(parse(Float64, SubString(line, start:start+10)), digits=5), round(parse(Float64, SubString(line, start+11:start+22)), digits=5)])
        elseif occursin("ksj:postedLandPrice", line) == true
            PointID += 1
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllPrices, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:address", line) == true
            IsInScope = occursin(Scope, line)
        elseif (occursin("ksj:acreage", line) == true) && (occursin(r"true|false", line) == false)
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllAcreages, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:currentUse", line) == true
            if occursin("住宅", line) == true #ここをいじれば用途も絞れる. いまは用途に「住宅」を含むもののみ取り出している
                IsInScope2 = IsInScope2 || true
            end
        elseif occursin("</ksj:LandPrice>", line) == true
            if IsInScope * IsInScope2 == true
                push!(InScopeCoordinates, AllCoordinates[PointID])
                push!(InScopePrices, AllPrices[PointID])
                push!(InScopeAcreages, AllAcreages[PointID])
            end
            IsInScope2 = false
        end
    end

elseif parse(Int, yy) == 12
    f = open(string("L01-", yy, "_13.txt"), "r")
        Data = readlines(f)
    close(f)
    for line in Data
        if occursin("gml:pos", line) == true
            start = match(r">", line).offset + 1
            push!(AllCoordinates, [round(parse(Float64, SubString(line, start:start+10)), digits=5), round(parse(Float64, SubString(line, start+11:start+22)), digits=5)])
        elseif occursin("ksj:plp", line) == true
            PointID += 1
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllPrices, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:as1", line) == true
            IsInScope = occursin(Scope, line)
        elseif (occursin("ksj:ac1", line) == true) && (occursin(r"true|false", line) == false)
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllAcreages, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:pu1", line) == true
            if occursin("住宅", line) == true
                IsInScope2 = IsInScope2 || true
            end
        elseif occursin("</ksj:LandPrice>", line) == true
            if IsInScope * IsInScope2 == true
                push!(InScopeCoordinates, AllCoordinates[PointID])
                push!(InScopePrices, AllPrices[PointID])
                push!(InScopeAcreages, AllAcreages[PointID])
            end
            IsInScope2 = false
        end
    end

elseif 13 <= parse(Int, yy) <= 21
    f = open(string("L01-", yy, "_13.txt"), "r")
        Data = readlines(f)
    close(f)
    for line in Data
        if occursin("gml:pos", line) == true
            start = match(r">", line).offset + 1
            push!(AllCoordinates, [round(parse(Float64, SubString(line, start:start+10)), digits=5), round(parse(Float64, SubString(line, start+11:start+22)), digits=5)])
        elseif occursin("ksj:postedLandPrice>", line) == true
            PointID += 1
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllPrices, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:address", line) == true
            if (yy == "16") && ((PointID == 1135) || (PointID == 1261) || (PointID == 1990)) #入力ミスされているデータを消す, ここでは東京だが都道府県によって除くIDを変える(もとのtxtファイルでid=〇〇となっている数字に1を足したものを入れる)
                IsInScope = false
            else
                IsInScope = occursin(Scope, line)
            end
        elseif (occursin("ksj:acreage", line) == true) && (occursin(r"true|false", line) == false)
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllAcreages, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:currentUse", line) == true
            if occursin("住宅", line) == true
                IsInScope2 = IsInScope2 || true
            end
        elseif occursin("</ksj:LandPrice>", line) == true
            if IsInScope * IsInScope2 == true
                push!(InScopeCoordinates, AllCoordinates[PointID])
                push!(InScopePrices, AllPrices[PointID])
                push!(InScopeAcreages, AllAcreages[PointID])
            end
            IsInScope2 = false
        end
    end

elseif 22 <= parse(Int, yy) <= 23
    f = open(string("L01-", yy, "_13.txt"), "r")
        Data = readlines(f)
    close(f)
    for line in Data
        if occursin("gml:pos", line) == true
            start = match(r">", line).offset + 1
            push!(AllCoordinates, [round(parse(Float64, SubString(line, start:start+10)), digits=5), round(parse(Float64, SubString(line, start+11:start+22)), digits=5)])
        elseif occursin("ksj:postedLandPrice>", line) == true
            PointID += 1
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllPrices, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:location", line) == true
            IsInScope = occursin(Scope, line)
        elseif (occursin("ksj:acreage", line) == true) && (occursin(r"true|false", line) == false)
            start = match(r">", line).offset + 1
            fin = match(r"</", line).offset - 1
            push!(AllAcreages, parse(Int, SubString(line, start:fin)))
        elseif occursin("ksj:currentUse", line) == true
            if occursin("住宅", line) == true
                IsInScope2 = IsInScope2 || true
            end
        elseif occursin("</ksj:LandPrice>", line) == true
            if IsInScope * IsInScope2 == true
                push!(InScopeCoordinates, AllCoordinates[PointID])
                push!(InScopePrices, AllPrices[PointID])
                push!(InScopeAcreages, AllAcreages[PointID])
            end
            IsInScope2 = false
        end
    end
end

save(string("DF_", yy, ".csv"), DataFrame(Coordinate = InScopeCoordinates, Year = fill(YearConvert(yy), length(InScopeCoordinates)), Price = InScopePrices, Acreage = InScopeAcreages); delim = ';')

年が変わるとたまーに仕様が変わったりして, 結構面倒なんですよね. 特に2016年は入力ミスで座標が被っちゃっているデータがあるので大変です(コードでは手で除いてあります). 国土地理院のページから欲しい年度・範囲のファイルをダウンロードして, xmlファイルをtxtファイルに書き換えてからご利用ください.

(以下余談) 経験則として, 土地や物件の価格\(\hspace{0.2em}P\hspace{0.2em}\)はその面積\(\hspace{0.2em}S\hspace{0.2em}\)に指数的に依存することが知られています.

$$ P = e^{aS+Q} $$
ここで, \(\hspace{-0.2em}\hspace{0.2em}Q\hspace{0.2em}\)は個々の土地や物件の「個性」による価格のズレを押し付けるための項です. したがって, \(\hspace{-0.2em}\hspace{0.2em}a\hspace{0.2em}\)を最小2乗フィットなどで適当に求めたうえで
$$ Q = \log{P}-aS $$
の分布を調べることで, 地価の傾向を調べることができます(だから, 面積の情報も一緒に記録する必要があったんですね). お暇でしたらぜひやってみてください. ただし, 上のコードで記録しているPriceは面積あたりの価格ですので, PriceにAcreageを乗じたものを上式の\(\hspace{0.2em}P\hspace{0.2em}\)として使いましょう.

脚注

  1. クリスマスも近いですしね…

  2. 出典:いらすとや

  3. 房総半島の最南端ポイントです

  4. 本数が少ないので, バスの時間にはご注意ください

  5. 夜はライトアップしているところもあるそうです. 素敵!

  6. そもそもあれは「東京」って銘打ってますし