カテゴリー
Python

符号無し整数で表現された日付に対して指定した月数や日数を引き算/足し算して返す

”符号無し整数で表現された日付”とは、例えば 2021年1月1日を 20210101 のような数値として表現するような方法の事です。

divmodというビルトイン関数が面白かったので応用例を作ってみました。コードは次の通りです。うるう年も考慮して結果を求めることが出来ます。

class date_as_integer:
    """符号無し整数で表現された日付に対する演算子をまとめたクラス
    A class of operators for dates represented by unsigned integers
    """

    @staticmethod
    def add_days(orig_date, duration):
        """8か6桁の符号無し整数(yyyyMMdd か yyMMdd)で表現された
        日付について、指定した日数を加算、または減算した結果を返す関数
        A function implementation that returns the result of
        adding or subtracting the specified number of days for a date
        represented by an 8- or 6-digit unsigned integer (yyyyMMdd or yyMMdd).
        """
        Y, _md = divmod(orig_date, 10000)
        M, D = divmod(_md, 100)
        M -= 1
        D -= 1

        def get_ndays_month(year):

            months_of_year = [
                [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], # not leap
                [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # leap year
            ]

            return months_of_year[
                year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
            ]

        def future(ndays, duration, Y, M):
            ndays = get_ndays_month(Y)[M]
            M += 1
            duration -= ndays
            if M > 11:
                M = 0
                Y += 1
            return ndays, duration, Y, M

        def past(ndays, duration, Y, M):
            M -= 1
            ndays = get_ndays_month(Y)[M]
            duration += ndays
            if M < 0:
                M = 11
                Y -= 1
            return ndays, duration, Y, M

        step_into_next_month = future if (duration >= 0) else past

        ndays = get_ndays_month(Y)[M]

        while not ndays > (D + duration) >= 0:

            ndays, duration, Y, M = step_into_next_month(ndays, duration, Y, M)

        D += duration

        return Y * 10000 + (M + 1) * 100 + D + 1


    @staticmethod
    def add_months(orig_date, duration):
        """6か4桁の符号無し整数(yyyyMM か yyMM)で表現された
        日付について、指定した月数を加算、または減算した結果を返す関数
        A function implementation that returns the result of
        adding or subtracting the specified number of months for a date
        represented by an 6- or 4-digit unsigned integer (yyyyMM or yyMM).
        """
        Y0, M0 = divmod(int(orig_date), 100)
        dy, dm = divmod(duration, 12)

        if M0 + dm > 12:
            Y1 = Y0 + dy + 1
            M1 = M0 + dm - 12
        else:
            Y1 = Y0 + dy
            M1 = M0 + dm

        return Y1 * 100 + M1


if __name__ == "__main__":
    print('Up and Down at new year boundary')
    for i in range(2):
        print(date_as_integer.add_days(20211231, i))
    for i in range(2):
        print(date_as_integer.add_days(20220101, -i))

    print('At boundary of leap day every 4 years')
    for i in range(3):
        print(date_as_integer.add_days(20200228, i))
    for i in range(3):
        print(date_as_integer.add_days(20200301, -i))

    print('''At boundary of leap day in a year that cannot divide by 100
(28 days are in February) ''')
    for i in range(2):
        print(date_as_integer.add_days(21000228, i))
    for i in range(2):
        print(date_as_integer.add_days(21000301, -i))

    print('''At boundary of leap day in a year that can divide by 400
(29 days are in February) ''')
    for i in range(3):
        print(date_as_integer.add_days(20000228, i))
    for i in range(3):
        print(date_as_integer.add_days(20000301, -i))

    print('Find the date N months later (or earlier)')
    for i in range(2):
        print(date_as_integer.add_months(200012, i))
    for i in range(2):
        print(date_as_integer.add_months(200101, -i))

    print('38 == 6 + 12 + 12 + 8')
    print(date_as_integer.add_months(200006, 38))
    print(date_as_integer.add_months(200006, -38))

実行すると次の様な表示結果を得ます。

Up and Down at new year boundary
20211231
20220101
20220101
20211231
At boundary of leap day every 4 years
20200228
20200229
20200301
20200301
20200229
20200228
At boundary of leap day in a year that cannot divide by 100
(28 days are in February) 
21000228
21000301
21000301
21000228
At boundary of leap day in a year that can divide by 400    
(29 days are in February) 
20000228
20000229
20000301
20000301
20000229
20000228
Find the date N months later (or earlier)
200012
200101
200101
200012
38 == 6 + 12 + 12 + 8
200308
199704

カテゴリー
Python 未分類

Excel 作業を自動化する為の Python 応用

列見出しを順番に繰り出す

ワークシートの列位置を区別するシンボルを連続的に発生させるジェネレータ(Python の予約語:yield 式を持つ特殊な関数)を作りました。

コード

def xl_col_name_gen():
  # 1st dimension (from A to Z)
  for i in range(0x41, 0x5b):
    yield '{:c}'.format(i) 
  # 2nd dimension (from AA to ZZ)
  for i in range(0x41, 0x5b):
    for j in range(0x41, 0x5b):
      yield '{:c}{:c}'.format(i,j)
  # 3rd dimension (from AAA to ZZZ)
  for i in range(0x41, 0x5b):
    for j in range(0x41, 0x5b):
      for k in range(0x41, 0x5b):
        yield '{:c}{:c}{:c}'.format(i,j,k)

この関数から最初に繰り出されるシンボルは’A'(0x41)で、Excel ワークシートの最も左側に位置する列を識別する為のものです。次に 繰り出されるのが’B’,順番に’C’,’D’,’E’‥と続き、’Z'(0x5a)まで繰り出されると次が’AA’に替わります。参照記事:ASCIIコード表

利用例

>>> gen = xl_col_name_gen()
>>> gen
<generator object xl_col_name_gen at 0x000001C788C98A50>
>>> next(gen)
'A'
>>> next(gen)
'B'
>>> next(gen)
'C'
>>> def func_A(gen):
...   print(next(gen), next(gen), next(gen))
...
>>> def func_B(gen):
...   print([item for item in gen])
...
>>> func_A(gen)
D E F
>>> func_B(gen)
['G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 
..... 途中略 .....
'YX', 'YY', 'YZ', 'ZA', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZK', 'ZL', 'ZM', 'ZN', 'ZO', 'ZP', 'ZQ', 'ZR', 'ZS', 'ZT', 'ZU', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ', 'AAA', 'AAB', 'AAC', 'AAD', 'AAE', 
..... 途中略 .....
 'ZYW', 'ZYX', 'ZYY', 'ZYZ', 'ZZA', 'ZZB', 'ZZC', 'ZZD', 'ZZE', 'ZZF', 'ZZG', 'ZZH', 'ZZI', 'ZZJ', 'ZZK', 'ZZL', 'ZZM', 'ZZN', 'ZZO', 'ZZP', 'ZZQ', 'ZZR', 'ZZS', 'ZZT', 'ZZU', 'ZZV', 'ZZW', 'ZZX', 'ZZY', 'ZZZ']
>>>

説明

  • Excel 作業を自動化するプログラムではランダムにセル番地を変える事はほとんどなく、列順アクセスなので、プログラムが見やすくする為ジェネレータを使いました。
  • ジェネレータとせず、生成したシンボルをリストやタプルで戻すシンプルな方法もありますが、これだと 26 + (26*26) + (26*26*26) = 18278 バイトのストレージが必要です。ところがジェネレータでは next メソッドを呼び出すと、次の yield 式が現れるまで演算を実行するだけなので、追加のストレージが要らないのが特長です。
  • このジェネレータで列名を表示させると ‘ZZZ’まで繰り出しますが、実際のところ「Excel の仕様と制限」のため、’XFD’ が利用可能な最大値です。
  • 冒頭からの繰り出しを何回も繰り返したい場合は、次のコード例のように gen = xl_col_name_gen() の代入を繰り返してください。
>>> gen = xl_col_name_gen()
>>> next(gen)
'A'
>>> next(gen)
'B'
>>> gen = xl_col_name_gen()
>>> next(gen)
'A'
>>> next(gen)
'B'
>>>

応用例(Pandas)

このジェネレータは科学実験やプログラムのテスト結果を Excel へまとめて保存しておきたい場合などに応用することが出来ます。

予想値と測定値(あるいは計算結果)が CSV やその他の手段を使い入手可能だとします。Pandas を使ってこのデータを処理し、分かったことを誰かへレポートする事例を想定してみます。参照記事:pandas documentation

import pandas as pd

df = pd.DataFrame({
    # 2 番目のテストだけ NG という想定のサンプルデータ
    '予想値': [ 543, 651, 995, 331 ],
    '測定値': [ 543, 654, 995, 331 ],
})

報告する相手がエンジニアリング分野の方であれば次の計算結果を示せば結果を理解するはずです。

>>> df[df['予想値'] != df['測定値']]
   予想値  測定値
1  651  654
>>>

プログラムのユニットテストであれば、例外をスローしてくれた方が都合がよく、無効同値クラスのテストが通った事をシンプルに証明できます。

>>> pd.testing.assert_series_equal(df['予想値'], df['測定値'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\someone\.venv\lib\site-packages\pandas\_testing\asserters.py", line 1077, in assert_series_equal
..... 途中略 .....
AssertionError: Series are different

Series values are different (25.0 %)
[index]: [0, 1, 2, 3]
[left]:  [543, 651, 995, 331]
[right]: [543, 654, 995, 331]
>>>

上記の何れでもない場合は万人向けのまとめ方が必要です。そこで Excel を登場させます。

予想値と測定値を照し合せた結果を表示させる列の名前を仮に「判定」とします。

col_names = df.columns.tolist() + ['判定']

先のジェネレータ式を使って Excel のセルの列名を必要な数だけ繰り出します(事例では3つ。つまり’A’,’B’,’C’)。

gen = xl_col_name_gen()
ref_cols = [next(xl_col_name_gen) for i in range(len(col_names))]

「判定」列へ埋め込む Excel 計算式の元となるフォーマット文字列を用意します。波かっこで囲まれた部分へ、後述の format関数へ与えた引数があてがわれます。空の波かっこ部分にセルの列名、rownoを含む波かっこ部分にセルの行番号がそれぞれ埋め込まれる想定です。

expr_fmt = '=({}{rowno}={}{rowno})'

無名関数: f を定義します。これはデータセットの行数回呼び出され、セル番地を伴うExcel式を文字列として返します。引数の idx は DataFrame のインデックスが渡されます。定数 2 が足されている理由は、Excel の行番号が 1 から開始されることと、列名の行が 1 行分置かれているからです。

f = lambda idx: expr_fmt.format(*ref_cols[:], rowno=idx + 2)

セル番地による参照を含んだ Excel 計算式を「判定」列へ埋め込みます。-1 という添え字を与えるとリストの末尾に位置する要素が得られます。

df[col_names[-1]] = df.index.map(f)

上記に説明したコードを連続的に実行させた結果を例示します。

>>> # 判定用の Excel 計算式を置く列を含めた列リスト
>>> col_names = df.columns.tolist() + ['判定']
>>> gen = xl_col_name_gen()
>>>
>>> # 使われている列の数だけ Excel の列名を繰り出す
>>> # (この例では 'A','B','C' の各列)
>>> ref_cols = [gen) for i in range(len(col_names))]
>>>
>>> # Excel へ埋め込む判定式のフォーマット
>>> expr_fmt = '=({}{rowno}={}{rowno})'
>>>
>>> # expr_fmt のプレースフォルダへ値を埋め込む為のラムダ式
>>> # + 2 している理由は Pandas のインデックスが 0 から始まる
>>> # のと、列名の行が置かれる次の行から評価式が置かれる為。
>>> f = lambda idx: expr_fmt.format(*ref_cols[:], rowno=idx + 2)
>>>
>>> # 「判定」という名前の新しい列を DataFrame へ加えつつ、
>>> # 評価式を1行ずつ埋め込んでゆく
>>> df[col_names[-1]] = df.index.map(f)
>>>

DataFrame に何が保存されたか確認してみましょう。

>>> df
   予想値  測定値        判定
0  543  543  =(A2=B2)
1  651  654  =(A3=B3)
2  995  995  =(A4=B4)
3  331  331  =(A5=B5)
>>>

このデータは Pandas の to_csv 関数を使うとファイルへ保存することが出来ます。データに日本語が含まれている場合、encodingを指定することで文字化けを防ぐことが出来ます。

df.to_csv('validate.csv',encoding='utf-8_sig', index = False)

保存された「validate.csv」というファイルをExcel へインポートしてみます。手順は次の通りです。

[メニュー]-[ファイル]-[開く]-《ダイアログから validate.csv を選択》-[テキストファイルウイザード1/3]-《”先頭行をデータの見出しとして使用する”をチェック。”次へ”》-[ テキストファイルウイザード2/3 ]-《”区切り文字”=”カンマ” 。”次へ” 》-[ テキストファイルウイザード3/3 ]-《”完了”》

インポートしたデータがExcel へ表示されます。「判定」列へ埋め込んだ式がインポート時に計算されて「TRUE」または「FALSE」の表示になっている事と、2番目に表示されているNGだったテスト結果を反映して「判定」が「FALSE」になっている事がポイントです。

確認すべきデータが大量にあるときは Excel の視覚効果を利用する事も可能です。C列をマウスで全選択して次の手順で「FALSE」と表示されたセルを網掛けにすることが出来ます。

[メニュー]-[条件付き書式..]-[セルの強調表示ルール…]-[指定の値に等しい]-《”次の値に等しいセルを書式設定:”←”FALSE”》

これなら NG だったテストが直ぐに探し出し易くなります。

カテゴリー
PowerShell Python

pip install -U pip 時に Fatal error in launcher: Unable to create process using ~ というエラーが発生した時の解決方法

かつて Python for Windows の v3.8.6 を c:\python-3.8.6-amd64 というフォルダへインストールして使っていました。

v3.8 系の新しいバージョン v3.8.10 へバージョンアップを試み、msi 形式をダウンロードページから入手しインストール。

インストールウイザードにデフォルト表示されたインストール先を変えずにそのまま  c:\python-3.8.6-amd64  へ上書きしました。

ところが、インストール後にこのインストール先の名前がどうにも気に入らなくなったので c:\python-3.8.10-amd64 へ変更しました。

フォルダ名変更を反映させる為レジストリも書き換えました。具体的には管理者モードで PowerShell を起動し次のコードを入力しました。事例は v3.8 なのでコピー&ペーストして再利用される際には十分お気を付けください

function Replace-PropValues {
    param(
        [string] $Path,
        [string] $Before,
        [string] $After
    )
    process {
        $regKey = (Get-Item -Path $path)
        $func = { $regKey.GetValue($args).Replace($Before,$After) }
        $regKey.Property.ForEach( {
            if ($_ -eq "(default)") {
                Set-ItemProperty -Path $regKey.PSPath -Name '(default)' `
                  -Value $func.Invoke('')
            }
            else {
                Set-ItemProperty -Path $regKey.PSPath -Name $_ `
                  -Value $func.Invoke($_)
            }
        })
    }
}
Replace-PropValues -Before "3.8.6" -After "3.8.10" `
  -Path "HKLM:\SOFTWARE\Python\PythonCore\3.8\InstallPath"
Replace-PropValues -Before "3.8.6" -After "3.8.10" `
  -Path "HKLM:\SOFTWARE\Python\PythonCore\3.8\PythonPath"

ついでにシステム環境変数 PATH に書かれていた古いインストール先の名前も変えこれで作業完了かと思い、pip のアップグレードを試みるため表題の通りのコマンドを入力したところ、エラーと遭遇しました。

C:\Users\foobar>pip install -U pip
Fatal error in launcher: Unable to create process using '"c:\python-3.8.6-amd64\python.exe"  "C:\python-3.8.10-amd64\Scripts\pip.exe" install -U pip': ??????????????????

10分間ほど googling の末に、次の2つのコマンド入力で解決できることが分かったので、投稿することにしました。

python -m pip uninstall pip
python -m ensurepip --upgrade

ensurepip の説明はこちら。なるほどねぇ…

因みに pipenv を使って仮想化していましたので次の作業も追加で必要でした。

python -m pip uninstall pipenv
python -m pip install --upgrade pipenv