Pythonでユーザーのホームディレクトリを取得する方法

今まで知らなかったメソッド(expanduser)を見つけたのでその紹介を兼ねて小ネタの紹介です。

※注: Windowsでも動作する様に書いていますが、手元にWindowsのPython環境を持っていないのでこの記事の内容はWindowsでは未検証です。

今回の記事でやるのはタイトルの通り、ユーザーのホームディレクトリの取得です。

ホームディレクトリの下に作業ディレクトリやログディレクトリを作ってプログラムで使うと言う状況はそこそこある事だと思っています。書き捨てのスクリプトであればホームディレクトリのパスを直書きしてしまえばいいのですが、どこかに公開する場合や自分で使いまわしたい場合はそれでは不便が起きます。僕の場合でもAWS環境と、複数台のMacを使っているので、ホームディレクトリを直書きしてしまうと使い回しに修正が必要です。そのため、ホームディレクトリは自動的に取得することが望ましく、この記事のテクニックが必要になります。

いくつか方法があるので順番に紹介していきます。ちなみに、サンプルコードではユーザー名がyutaroだと仮定します。そのため結果として得られるホームディレクトリは ‘/Users/yutaro’ です。

環境変数を使う方法

一番簡単なのは環境変数から取得してしまうことですね。Mac/LinuxではHOME、WindowsではHOMEPATHやUSERPROFILEという環境変数に格納されています。次の様にするとどの環境でも動く様に書くことができます。orで繋いで最初に見つかったものを採用しているだけです。なお、絶対Windowsでは使わないよ、って場合はもうHOMEだけ見たら良いです。

import os


home_directory = (
    os.environ.get('HOME') or
    os.environ.get('HOMEPATH') or
    os.environ.get('USERPROFILE')
)
print(home_directory)
# /Users/yutaro

上記の方法は特別なメソッドの知識とかいらないのですがご覧の通り記述量が多いので他の方法をつづいて紹介していきます。以降の方法の方がお勧めです。

osモジュールのメソッドを使う方法

(環境変数の取得もosモジュールでやってるので表題困りましたが、) osモジュールにはexpanduserという専用のメソッドを持っています。これは与えられたパスの先頭の ~ (チルダ) や ~user という文字列をホームディレクトリのパスに置換してくれるものです。

参考: os.path — 共通のパス名操作 — Python 3.11.4 ドキュメント

いくつか実験したのでコードを載せておきます。 ‘~user’って文字列でもいい様なことがドキュメントに書いてますが、これはuserじゃなくてOSのユーザー名を入れないといけない様です。環境ごとの記述の差異を吸収してほしいという今回の記事の主題的にはちょっとダメですね。素直に~だけ使いましょう。

# チルダをHOMEディレクトリに書き換えてくれる。
print(os.path.expanduser("~"))
# /Users/yutaro

# ドキュメントの ~user は ~userという文字列を指してるものではないらしく、~userはそのまま。
print(os.path.expanduser("~user"))
# ~user

# ~ユーザー名、僕の場合は ~yutaro は置換される。
print(os.path.expanduser("~yutaro"))
# /Users/yutaro

# ~の後ろにそのまま文字列が続いていたら置換されない。
print(os.path.expanduser("~work"))
# ~work

# ~の後ろに小ディレクトリを書いておくことができる。
print(os.path.expanduser("~/folder/subfolder"))
# /Users/yutaro/folder/subfolder

# ~が先頭でない場合は置換されない。
print(os.path.expanduser("/~/subfolder"))
# /~/subfolder

最後にpathlibって別のモジュールを使った方法もあるのでそれも紹介します。

pathlibモジュールを使った方法

こちらは、home()っていうズバリなメソッドを持ってます。
参考: pathlib — オブジェクト指向のファイルシステムパス — Python 3.11.4 ドキュメント

これが一番いいかな。(ネックは、pathlibの存在を忘れがちなことくらいか。)

from pathlib import Path


print(Path.home())
# /Users/yutaro

# 実はデータ型が違う
print(type(Path.home()))
# <class 'pathlib.PosixPath'>

print(type(os.path.expanduser("~")))
# <class 'str'>

内部的には、実質的に os.path.expanduser(“~”) と同等の処理をやってるみたいですね。ただし、上のサンプルコードの後半で見ている通り、Path.home()はPathオブジェクトを返してくれているので、その後のパス操作がより直感的になります。

話のついでにもう一つ紹介しておくと、 pathlibもexpanduserを持ってます。ただ、これは文字列を受け付けてくれないのでosのそれより使い勝手が悪いです。(個人の感想)

# Path.expanduser は文字列を受け付けないので例外が発生する。
try:
    print(Path.expanduser("~"))
except Exception as e:
    print(e)
# 'str' object has no attribute '_drv'


from pathlib import PosixPath


# PosixPath型に変換して使う
print(Path.expanduser(PosixPath(("~"))))
# /Users/yutaro

まとめ

以上で、Pythonでホームディレクトリを取得する方法をまとめてきました。ハードコーディングをやめてこれらの方法を使うとコードの使い回しがよりやりやすくなると思います。

個人的にはイチオシは pathlibの Path.home() ですかね。ただ自分でも忘れてosで環境変数取りに行くことが多そうですが。

interpolateメソッドを利用したpandasデータの欠損値の補完

データ分析を行う際、データセットに欠損値(NaNやNoneなど)が含まれていることはよくあります。これらの欠損値をどのように取り扱うかは、分析結果に大きな影響を及ぼすため、重要なステップとなります。

Pandasを使う場合、これらの欠損値に対応する一番簡易的な方法はfillna()を使った定数による補完です。もしくはdropna()を使ってそのデータを消す事もあるかもしれませんね。

しかし、状況によってはすべての欠損値を単一の値で補完するのは、データの分布や傾向を歪める可能性があります。また、時系列データなどでは欠損値が発生したレコードをdropできない事もあるかもしれません。周期がずれたりしますからね。

そこで使えるのが、掲題のinterpolate()メソッドです。これを使うとここの欠損値の前後の値を使った補完など多様な補完ができます。特に引数を指定しなければ線型補完です。

参考: pandas.Series.interpolate — pandas 2.0.3 documentation

まず基本的な使い方を見ていきましょう。欠損値を含む単純なSeriesデータを用意してやってみます。

import pandas as pd
import numpy as np


s = pd.Series([0, 2, np.nan, np.nan, np.nan, 10])
print(s)
"""
0     0.0
1     2.0
2     NaN
3     NaN
4     NaN
5    10.0
dtype: float64
"""

print(s.interpolate())
"""
0     0.0
1     2.0
2     4.0
3     6.0
4     8.0
5    10.0
dtype: float64
"""

はい、等差数列で補完してくれていますね。単純な例なのでとても自然な結果になっています。補完の方法はmethod引数で指定でき、デフォルトは”linear”です。
他には、次の様な値が使えます。
– linear ・・・ 線型補完。これがデフォルト値。
– ffill または pad ・・・ 前の値。
– bfill, backfill ・・・ 後ろの値。
– nearest ・・・ 最も近い値。
– polynomial ・・・多項式補完 (orderで次数を指定する)。
– spline ・・・スプライン補完 (orderで次数を指定する)。

他にも indexの値を考慮してくれるindexやvalue、時系列で使いやすそうなtimeなどもありますね。(実際に指定できる文字列は他にもあり、その種類はかなり多いです。公式ドキュメントの参照をお勧めします。)

いくつかやってみます。

s = pd.Series([0, 2, np.nan, np.nan, np.nan, 10, 9, np.nan, np.nan, 6])
print(s.values)
# [ 0.  2. nan nan nan 10.  9. nan nan  6.]

# 線型補完
print(s.interpolate(method='linear').values)
# [ 0.  2.  4.  6.  8. 10.  9.  8.  7.  6.]

# Pad。ffillも同じ結果。前の値を使う。
print(s.interpolate(method='pad').values)
# [ 0.  2.  2.  2.  2. 10.  9.  9.  9.  6.]

# bfill。backfill。後ろの値を使う。
print(s.interpolate(method='bfill').values)
# [ 0.  2. 10. 10. 10. 10.  9.  6.  6.  6.]

# 最も近い値。
print(s.interpolate(method='nearest').values)
# [ 0.  2.  2.  2. 10. 10.  9.  9.  6.  6.]

# 多項式補完
print(s.interpolate(method='polynomial', order=2).values)
# [ 0.          2.          4.43062201  7.29186603  9.50717703 10.
#  9.          7.88516746  6.88516746  6.        ]

# スプライン補完
print(s.interpolate(method='spline', order=2).values)
# [ 0.          2.          5.30198447  7.26402071  8.60569456 10.
#  9.          8.90854185  7.76876618  6.        ]

polynomial と spline の違いがわかりにくいと思いますが、ざっくり説明すると次の様になります。

polynomial:多項式補間では、欠損値を補完するために多項式関数が使用されます。指定した次数の多項式がデータにフィットされ、その多項式関数に基づいて欠損値が補完されます。しかし、データの点が多い場合や次数が高い場合、多項式補間はデータに過剰にフィットする(オーバーフィッティングする)傾向があります。

spline:スプライン補間では、データセット全体を通じて一つの関数が使用されるのではなく、各データ点の間に別々の多項式(通常は3次)がフィットされます。これらの多項式は、データ点において連続性と滑らかさを保つように選ばれます。スプライン補間は、より滑らかな曲線を生成し、オーバーフィッティングを避けるために通常は低次の多項式(たとえば3次)が使用されます。

この二つの選択に限った話ではありませんが、適切な補間方法を選択する際には、データの性質と分析の目的を考慮することが重要です。

pandasのSeriesのlocには関数も渡せる

何となくpandasのドキュメントを眺めていたら見つけた小ネタの紹介です。
この記事を読むと、pandasのSeriesをもっと手軽に値で絞り込める様になります。
参考: pandas.Series.loc — pandas 2.0.3 documentation

pandasのlocといえば、自分としてはDataFrameで使うことが多いプロパティですが、もちろんSeriesにも実装されています。そして、これを使うとindexの値に従って要素を絞り込むことができます。

今回見つけたのは、このlocにcallable、要するにメソッドが渡せるってことです。渡したメソッドにSeriesの値が渡され、その結果がTrueのものに絞り込まれます。

こんな感じで使えます。例えば値が3以上の要素だけに絞り込む例です。

import pandas as pd

# Seriesを作成
s = pd.Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e'])

# 値が3以上の要素だけを絞り込む
s[lambda x: x>=3]
"""
c    3
d    4
e    5
dtype: int64
"""

上記の例だとメリットがわかりにくいかと思います。と言うのもpandasに慣れてる人だったら次の様に書けることがわかってると思いますし、タイプ数も少なく可読性も高いからです。

s[s>=3]
"""
c    3
d    4
e    5
dtype: int64
"""

単純な大小比較や一致不一致ではなく、もっと複雑な判定を行うメソッドを適用するときなどは、このlocにメソッドを渡すやり方が便利に使えますね。

僕が個人的に気に入ったのは、この絞り込みが特定の変数に格納されていないSeriesについても使えると言うことです。
「特定の変数に格納されていないSeries」ってのは、例えばDataFrameのvalues_counts()メソッドなどを実行した結果として得られる値などの形で取得されます。

例えば、dfというDataFrameがあるとして、そのcolumn_name列の値の出現回数を数え、そのうち出現回数が10以上のものだけを取り出すとしましょう。

通常であれば、value_counts()の結果を何かの変数に格納して実行するか、もしくは2回value_counts()を実行する非効率に我慢するかして次の様に実装します。

# カウント結果を一度変数に格納する場合
count_sr = df["column_name"].value_counts()
count_sr[count_sr >= 10]

# value_countsを2回実行する場合
df["column_name"].value_counts()[df["column_name"].value_counts() >= 10]

これが、locにメソッドが渡せることを知っていると次の様に書けます。

# 無駄な変数も定義しないし、value_counts()の実行も1回でいい書き方
df["column_name"].value_counts()[lambda x: x>=10]

個人的に、「ある列の要素ごとに数を数えて、一定件数以上データがあったものだけ残す処理」ってのをやることが頻繁にあり、Seriesのfiterメソッドが値ではなくindexにしか使えないことを日々残念に思っていた自分にとってはめっちゃ嬉しいテクニックだったので紹介しました。

ちなみに、DataFrameのlocも同じ様にcallableを渡せます。こっちはあまり使い道が思いつかないですね。
参考: pandas.DataFrame.loc — pandas 2.0.3 documentation

Jupyter notebook ファイルをモジュールとして import する

タイトルの通りで、Jupyter notebookファイル (.ipynb ファイル)をモジュールとしてインポートする方法を紹介します。

僕は普段のコーディングをJupyterでやっているので、自分で使う汎用的なモジュールを作るときも一度Jupyterで作って.pyファイルに移行するという手順で作っていました。一回完成させて仕舞えばそれでいいのですが、作りかけのものを別のプログラムで使いたいとか、それに限らず、あるnotebookで定義した関数を別のnotebookで使いたい、ってときにこれまでセルの中身をコピペしてたのですが、実はnotebookファイルのままimportできるという噂を聞き試しました。

機械学習の前処理とか何度も同じコードを書いてるので使いまわせる様になると便利そうです。

使うのはこちらの importnb というライブラリです。
参考: importnb · PyPI

とりあえず試してみましょう。
importされる側のnotebookファイルを以下の内容で作ります。importできることの確認だけなので、適当なメソッドと定数が定義されているだけのファイルです。

ファイル名は sub_file.ipynb としました。

def foo():
    return "bar"


hoge = "hoge"

では、このファイルをimportしてみましょう。

importnb sub_file とかで済むと簡単なのですが、やや独特な記法でimportします。先ほどのsub_file.ipynb は閉じて新しいnotebookファイルで以下の様に書きます。

from importnb import imports


with imports("ipynb"):
    import sub_file

これでimportできました。
メソッド foo や 変数 hoge が使えます。

print(sub_file.foo())
# bar

print(sub_file.hoge)
# hoge

少し検証してみたのですが、このライブラリはimportされるnotebookの中身によっては注意して使う必要があります。というのも、notebookを import するときにimport されるnotebookの全てのセルが実行されるのです。なので、何かファイルを書き出す処理があればimportした時点でファイルを書き出ししますし、重い処理があれば時間かかりますし、外部APIを叩く処理が入っていたら外部APIを叩きます。

宣言されているメソッドやクラス、変数だけを持ってきて使えると言うわけでは無いのでimportするnotebookは慎重に選びましょう。

というか、その確認作業をするのであれば .py ファイルに書き出すとか必要なセルだけコピペして持ってくるといった対応をする方が早いことが多く、この importnb を使う場面ってかなり限られるなぁと言うのが自分の所感です。

頻繁に使っうMeCabを使った前処理とかワードクラウドクラウド作成とか汎用的なSQLとか自分が頻繁に使うメソッドや定数をまとめた神notebook集を用意したりするとまた活用の幅も出てくるかもしれませんね。

もう一点、検証時に気付いた注意点があります。これ、notebookのセルを全て実行するので、その中に一つでもエラーになるセルがあったらimportを失敗します。

そのエラーになるセルより先に実行されたセルの中身だけimportされるのではなく、何もimportされない結果になります。これも注意しましょう。

Excel VBA で J-Quants APIを実行する

自分はもう10年以上使っているのですが意外と知られていない技術として、Excel VBAではHTTPアクセスを用いてWebサイトの情報取得やWeb APIの実行ができます。もしかしたらニーズがあるかもしれない技術なので紹介します。

APIの例として J-Quants API を利用しますが他のAPIでも同様に使えると思います。J-Quants APIを選定したのは利用手順の中でPOSTやGETやヘッダーの設定等いろいろ技術が必要で網羅的な紹介ができるからです。

このブログでは普段は暗黙のうちにOSがMacであることを前提としていますが、この記事に限ってはWindows前提です。MacのExcelでは動作しないと思います。

APIの利用方法自体はPython版の記事があるのでこちらをご参照ください。照らし合わせながら見ると、Excel VBA の XMLHTTP60 オブジェクトの使い方が分かってくると思います。

参考: J-Quants API の基本的な使い方

参照設定

以下の二つを参照設定しておいてください。XMLのほうがhttpアクセスに必要です。正規表現のほうは返ってきたJSONから必要な部分を取得するのに使います。VBAはJSONの扱いが不便なので、何か事情が無ければPython等の他の言語をお勧めします。

  • Microsoft XML, v6.0
  • Microsoft VBScript Regular Expressions 5.5

リフレッシュトークンの取得関数

リフレッシュトークンを取得する関数のコードは以下のようになります。

Public Function get_refresh_token(email As String, passoword As String) As String
    Dim objXMLHTTP As New XMLHTTP60
    Dim re As New RegExp
    Dim mc As MatchCollection
    Dim account_data As String
    Dim auth_user_url As String
    
    account_data = "{""mailaddress"": """ & email & """,""password"": """ & passoword & """}"
    auth_user_url = "https://api.jquants.com/v1/token/auth_user"
    
    Call objXMLHTTP.Open("POST", auth_user_url, False)
    Call objXMLHTTP.send(account_data)
    Do While objXMLHTTP.readyState <> 4
                DoEvents
    Loop
    
    ' Rehresh Tokenを取り出す正規表現
    re.Pattern = "refreshToken"": ""([^""]+)"""
    
    Set mc = re.Execute(objXMLHTTP.responseText)
    get_refresh_token = mc.Item(0).SubMatches(0)
    
End Function

12行目から15行目までが、APIにデータをPOSTして結果を待っている部分です。メソッド(POST)、URLをopenで指定して、sendするときにPOSTするデータを渡しています。この構文を覚えておくと大抵のAPIは使えます。GETメソッドの時はPOSTするデータはないのでsendの引数は空でよいです。

戻ってくるデータはJSONの文字列なので、正規表現で取り出してます。

idトークンの取得関数

リフレッシュトークンが取得出来たら次はidトークンです。これはリフレッシュトークンを組クエリパラメーターでPOSTします。

Public Function get_id_token(refresh_token As String) As String
    Dim objXMLHTTP As New XMLHTTP60
    Dim re As New RegExp
    Dim mc As MatchCollection
    Dim auth_refresh_url  As String

    auth_refresh_url = "https://api.jquants.com/v1/token/auth_refresh?refreshtoken=" & refresh_token
    
    Call objXMLHTTP.Open("POST", auth_refresh_url, False)
    Call objXMLHTTP.send
    Do While objXMLHTTP.readyState <> 4
                DoEvents
    Loop
    
    
    ' id Tokenを取り出す正規表現
    re.Pattern = "idToken"": ""([^""]+)"""
    
    Set mc = re.Execute(objXMLHTTP.responseText)
    get_id_token = mc.Item(0).SubMatches(0)
    

ほとんど同じですね。

メインのAPIを実行する関数

idトークンが取得出来たら目当てのAPIを取得する関数を実行します。とりあえず時系列データを取ってみましょうか。

JSONで各日の4本値データが返ってくるので、1日分ずつ取得して Sheet1 のセルに張り付ける処理にしました。この時点では、まだ1日分のデータがJSON形式になっているので、Excel や VBAで利用するにはもう一段階パースする必要がありますが、ここまでできればあとは手間だけの問題でしょう。

先ほどまでのTokenの取得と違って、リクエストのヘッダーを設定しないといけないのでその処理が入っています。

Public Sub get_price(id_token As String, code As String)
    Dim objXMLHTTP As New XMLHTTP60
    Dim re As New RegExp
    Dim mc As MatchCollection
    Dim daily_quotes_url As String
    Dim i As Integer

    ' daily_quotes_urlを構築
    daily_quotes_url = "https://api.jquants.com/v1/prices/daily_quotes?code=" & code

    Dim from_ As String
    Dim to_ As String
    Dim headers As Object
    Dim daily_quotes_result As Object
    Dim daily_quotes_df As Object
    
    Call objXMLHTTP.Open("GET", daily_quotes_url, False)
    Call objXMLHTTP.setRequestHeader("Authorization", "Bearer " & id_token)
    Call objXMLHTTP.send
    Do While objXMLHTTP.readyState <> 4
                DoEvents
    Loop
    
    ' 1日分のデータにマッチする正規表現
    re.Pattern = "{[^{}]*Date[^{}]*}"
    re.Global = True
    
    Set mc = re.Execute(objXMLHTTP.responseText)
    ' セルに出力
    For i = 0 To mc.Count - 1
        Sheet1.Cells(i + 1, 1) = mc.Item(i)
    Next i
End Sub

各関数と処理を実行する

一通り関数を作りましたので、次のプロシージャを使って呼び出しましょう。

Sub main()
    Dim refresh_token As String
    Dim id_token As String
    
    refresh_token = get_refresh_token("{メールアドレス}", "{パスワード}")
    id_token = get_id_token(refresh_token)
    Call get_price(id_token, "{証券コード}")
    
End Sub

これで動作するはずです。

Pythonを覚えて以来、この種の処理はほとんど全部Pythonでやるようになりましたが、まだまだデータ加工でExcelの出番が発生することはあり、Excel VBA でデータ取得から一貫して行えると便利な場面もあると思います。

とはいえ、通常はVBAはJSONの扱いが不便すぎるので、Pythonでデータ取得スクリプト書いた方が早かったりもするのですがPythonが使えない環境では重宝するでしょう。