山pの楽しいお勉強生活

勉強の成果を垂れ流していきます

PySparkでDataFrame.cacheはMEMORY_AND_DISKレベルキャッシュされる

概要

※2021/12/17時点の最新版であるPySpark3.2.0の情報です

詳細

Xiaomi Pad 5(MIUI12)を複数アカウント(ユーザー)で使用する

はじめに

  • Xiaomi Pad5を購入したものの、デフォルトでは複数ユーザーで使用ができない?
    • 端末というより、OSであるMIUIの仕様?
  • とりあえず複数ユーザーで使用できるようになったので手順をメモする
    • ただし、色々制限はかかっているの注意。詳細は下記参照
  • 他にも良い手順があったらコメントでもTwitterでも良いので教えて下さい

わかる人向けの手順概要

※よくわからない人は下記にキャプチャ付きの詳細手順あります。

  1. 設定 → デバイス情報
  2. MIUIバージョンを10回タップ
  3. 開発者向けオプションを有効
  4. 追加設定 → 開発者向けオプション
  5. デフォルト値にリセット
    • ※全然関係ない項目のように見えるがここが一番のハマりポイント
  6. 最下部のMIUI最適化をオフ
  7. 複数ユーザー → 追加
  8. 切り替え
  9. Google関連の設定はOFFにして初期設定をする

よくわかっていないこと(困っていること)

  • ログイン時に追加したマルチユーザーでログインできない
    • 所有者(管理者)でログイン後に、設定からユーザを切り替える必要がある
    • Activity Launcher というアプリでユーザの切り替えのショートカットを作ることでとりあえずは対応
  • 追加したユーザーの初期設定でインターネットに繋がらない
    • Google関連の設定をONにして初期設定をするを行うと永遠に終わらない

詳細手順

  1. 設定 → デバイス情報 を表示
  2. MIUIバージョンを10回タップ
    • MIUIバージョンを複数回タップ
    • 何回かタップしていると「デベロッパーになるまであとXステップです。」と表示される。
    • デベロッパーになるまであと1ステップです。
  3. 規定回タップすると「これでデベロッパーになりました!」と表示される。
    • これでデベロッパーになりました!
  4. 追加設定内に「開発者向けオプション」が表示されているのでタップ
    • 追加設定の開発者向けオプション
  5. 開発者向けオプション内のデフォルト値にリセットを複数回タップする
    • ここからが本記事のキモ。私の環境だと「MIUIの最適化をオンにする」が表示されなかったので以下の操作を行う。
    • デフォルト値にリセットを複数回タップ
    • 「自動入力の開発者向けオプションをリセットしました」と表示される。
      • この設定は自動入力の設定なのでこの表示は正しい
    • 自動入力の開発者向けオプションをリセットしました
  6. 何回かタップしていると「MIUIの最適化をオンにする」が表示されるのでオフに切り替える
    • MIUIの最適化をオンにする
  7. 重要な警告を熟読してから同意
    • 重要な警告
  8. 複数ユーザーという項目が追加されているので選択
  9. オンに切り替え
  10. ユーザーまたはプロファイルを追加 → ユーザを追加する。
    • 複数ユーザー、ON、ユーザーまたはプロファイルを追加
  11. 追加されたユーザをタップして切り替える
    • yamapに切り替える
  12. ユーザの初期設定で「 Google関連の設定はOFF」にする

参考

GitHub ActionsでビルドしたドキュメントをGitHub Pagesで表示する

まとめ

はじめに

GitHub Pagesの存在は知っていてもprivateリポジトリで使用できないと思っている方は多いと思いますが、2021/01/21より「GitHub Enterprise Cloud 」であればprivateリポジトリで使用できるようになっています。(正確にはアクセス制御ができるようになった)

github.blog

変更方法などは以下のドキュメントを参照

docs.github.com

とは言え、GitHub Pagesの管理はそれなりに面倒です。 そこで、GitHub Actionsを併用する事でGitHubのイベントをトリガーにしてGitHub Pagesを簡単に更新する事ができたのでまとめておきます。

よくある問題

  • CIでドキュメントをビルドしているが有効活用されない
  • ビルドしたドキュメントを置く場所がない
  • ビルドしたドキュメントにアクセスするのが面倒

手順

具体的なサンプル

注意点

参考

  • 公式ドキュメントはかなり充実しています

PySparkではDataFrameのjoinでorderは維持されない

概要

  • PySparkのDataFrameではjoinした際にorderは維持されない
    • 正確にはshuffleが行われる
  • orderは出力直前に行うのが鉄則

再現コード

from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df1 = spark.createDataFrame(
    [
        ['user_01'],
        ['user_02'],
        ['user_03'],
        ['user_04'],
        ['user_05'],
    ],
    ['id'],
)
df2 = spark.createDataFrame(
    [
        ['user_01', '1'],
        ['user_02', '2'],
        ['user_03', '3'],
        ['user_04', '4'],
        ['user_05', '5'],
    ],
    ['id', 'c1']
)
print('order byされているdf1')
df1 = df1.orderBy('id')
df1.show()

print('order byされているdf2')
df2 = df2.orderBy('id')
df2.show()

print('join後')
df1.join(df2, 'id').show()
order byされているdf1
+-------+
|     id|
+-------+
|user_01|
|user_02|
|user_03|
|user_04|
|user_05|
+-------+

order byされているdf2
+-------+---+
|     id| c1|
+-------+---+
|user_01|  1|
|user_02|  2|
|user_03|  3|
|user_04|  4|
|user_05|  5|
+-------+---+

join後
+-------+---+
|     id| c1|
+-------+---+
|user_03|  3|
|user_02|  2|
|user_01|  1|
|user_04|  4|
|user_05|  5|
+-------+---+

参考

PySparkのDataFrameでは同名のカラムが許容される

概要

  • PySparkのDataFrameでは同名のカラムが許容される
  • select などカラム名を指定する処理時に例外が発生する
  • カラム名を再定義、別名を付ける事で回避が可能

再現コード

作成時にカラム名が重複

from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df99 = spark.createDataFrame(
    [
        [1, 'a', 'b'],
        [2, 'aa', 'bb'],
    ],
    ['id', 'c1', 'c1'],
)
df99.show()
+---+---+---+
| id| c1| c1|
+---+---+---+
|  1|  a|  b|
|  2| aa| bb|
+---+---+---+

※このパターンはあまりない気がする

joinした結果カラム目が重複

from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df1 = spark.createDataFrame(
    [
        [1, 'a', 'b'],
        [2, 'aa', 'bb'],
    ],
    ['id', 'c1', 'c2'],
)
df2 = spark.createDataFrame(
    [
        [1, 'x', 'y'],
        [2, 'xx', 'yy'],
    ],
    ['id', 'c1', 'c2'],
)
merged_df = df1.join(df2, 'id')

# 重複カラムの存在は可能
merged_df.show()

# 重複していないカラムのselectは可能
merged_df.select('id').show()

# 重複しているカラムのselectは例外発生
merged_df.select('id', 'c1')
+---+---+---+---+---+
| id| c1| c2| c1| c2|
+---+---+---+---+---+
|  1|  a|  b|  x|  y|
|  2| aa| bb| xx| yy|
+---+---+---+---+---+

+---+
| id|
+---+
|  1|
|  2|
+---+

---------------------------------------------------------------------------
AnalysisException                         Traceback (most recent call last)
<ipython-input-20-69f4fa3aa508> in <module>
     24 
     25 # 重複しているカラムのselectは例外発生
---> 26 merged_df.select('id', 'c1')

/spark/python/pyspark/sql/dataframe.py in select(self, *cols)
   1419         [Row(name=u'Alice', age=12), Row(name=u'Bob', age=15)]
   1420         """
-> 1421         jdf = self._jdf.select(self._jcols(*cols))
   1422         return DataFrame(jdf, self.sql_ctx)
   1423 

/usr/local/lib/python3.7/site-packages/py4j/java_gateway.py in __call__(self, *args)
   1303         answer = self.gateway_client.send_command(command)
   1304         return_value = get_return_value(
-> 1305             answer, self.gateway_client, self.target_id, self.name)
   1306 
   1307         for temp_arg in temp_args:

/spark/python/pyspark/sql/utils.py in deco(*a, **kw)
    135                 # Hide where the exception came from that shows a non-Pythonic
    136                 # JVM exception message.
--> 137                 raise_from(converted)
    138             else:
    139                 raise

/spark/python/pyspark/sql/utils.py in raise_from(e)

AnalysisException: Reference 'c1' is ambiguous, could be: c1, c1.;

対応方法

toDFでカラムを指定して新しくDataFrameを作る

from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df1 = spark.createDataFrame(
    [
        [1, 'a', 'b'],
        [2, 'aa', 'bb'],
    ],
    ['id', 'c1', 'c2'],
)
df2 = spark.createDataFrame(
    [
        [1, 'x', 'y'],
        [2, 'xx', 'yy'],
    ],
    ['id', 'c1', 'c2'],
)
merged_df = df1.join(df2, 'id')

# カラムを指定して新しくDataFrameを作る
df88 = merged_df.toDF('id', 'df1_c1', 'df1_c2', 'df2_c1', 'df2_c2')
df88.show()
+---+------+------+------+------+
| id|df1_c1|df1_c2|df2_c1|df2_c2|
+---+------+------+------+------+
|  1|     a|     b|     x|     y|
|  2|    aa|    bb|    xx|    yy|
+---+------+------+------+------+

https://spark.apache.org/docs/3.0.0/api/python/pyspark.sql.html#pyspark.sql.DataFrame.toDF

aliasをつけてselectする

from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = SparkSession.builder.getOrCreate()
df1 = spark.createDataFrame(
    [
        [1, 'a', 'b'],
        [2, 'aa', 'bb'],
    ],
    ['id', 'c1', 'c2'],
)
df2 = spark.createDataFrame(
    [
        [1, 'x', 'y'],
        [2, 'xx', 'yy'],
    ],
    ['id', 'c1', 'c2'],
)
df1 = df1.alias('df1')
df2 = df2.alias('df2')
merged_df = df1.join(df2, 'id')

# showの見た目は変わらない
merged_df.show()

# alias付きで指定すると取得が可能
merged_df.select('id', 'df1.c1', 'df1.c2', 'df2.c1', 'df2.c2').show()
+---+---+---+---+---+
| id| c1| c2| c1| c2|
+---+---+---+---+---+
|  1|  a|  b|  x|  y|
|  2| aa| bb| xx| yy|
+---+---+---+---+---+

+---+---+---+---+---+
| id| c1| c2| c1| c2|
+---+---+---+---+---+
|  1|  a|  b|  x|  y|
|  2| aa| bb| xx| yy|
+---+---+---+---+---+

https://spark.apache.org/docs/3.0.0/api/python/pyspark.sql.html#pyspark.sql.Column.alias

fstringの中でdictionary、setの内包表記を使用する

結論

l = ['a', 'b', 'c']
s1 = f'{ {s:s for s in l} }'  # 中括弧の後にスペースが必要
assert s1 == "{'a': 'a', 'b': 'b', 'c': 'c'}"

s2 = f'{ {s for s in l} }'  # 中括弧の後にスペースが必要
assert s2 == "{'b', 'c', 'a'}"

※setの方は順番は保証されないので↑のassertは失敗する場合がある

詳細とか

fstring内でdictionaryやsetの内包表記を普通に記載すると内包表記の記載がそのまま文字列となる

l = ['a', 'b', 'c']
s3 = f'{{s:s for s in l}}'
assert s3 == "{s:s for s in l}"

s4 = f'{{s for s in l}}'
assert s4 == "{s for s in l}"

これは中括弧は中括弧でエスケープできるという仕様のため。pep-0489で定義されている。

www.python.org

で、どうすれば良いかというとスペースを入れることでエスケープを回避する。こちらについでも同じドキュメント内に記載がある。

www.python.org

Pythonのユニットテストでimportされている変数を上書きする

結論

  • 直接モジュールの変数を上書きすれば良い
  • ただし、importされた時点でそのモジュールの変数として扱われる事に注意

※文字で見ても良くわからないと思うので下記のコードを参照

ケース1(テスト対象に直接変数がimportされている場合)

テスト対象のコード群

# a.py
from define import HOGE
class TargetClass:
    def get_hoge(self):
        return f"aaa_{HOGE}_bbb" # ここのHOGEを入れ替えたい
# define.py
HOGE = 'hoge'

ユニットテスト

import a
from a import TargetClass

def test_hoge():
    a.HOGE = 'piyo' # モジュールをインポートしてモジュールの変数を直接入れ替える
    target_class = TargetClass()
    assert target_class.get_hoge() == 'aaa_piyo_bbb'

ケース2(テスト対象で使用されている別モジュールで直接変数がimportされている場合)

テスト対象のコード群

# a.py
from b import get_hoge
class TargetClass:
    def get_hoge2(self):
        return f"aaa_{get_hoge()}_bbb"
# b.py
from define import HOGE
def get_hoge():
    return HOGE # ここのHOGEを入れ替えたい
# define.py
HOGE = 'hoge'

ユニットテスト

import b
from a import TargetClass

def test_hoge2():
    b.HOGE = 'piyo' # モジュールをインポートしてモジュールの変数を直接入れ替える
    target_class = TargetClass()
    assert target_class.get_hoge2() == 'aaa_piyo_bbb'

※結局やっているのはケース1と同じで直接bをインポートして値を入れ替える

経緯とか

  • ↑に書いた通りのことをやりたかったが調べ方もわからずハマった
  • unittest.patch だと、Mockになってしまうので、違うそうじゃない感。

蛇足

  • ケース2のようにできるのであれば、定義の方を変更できそうに見えるが通常はうまくいかない
  • 下記のユニットテスト2のように、入れ替えたいモジュール(今回であれば b )が呼び出される前であればうまくいくが、一度呼び出すとモジュール内に直接定義されるようで、想定通りにならない

テスト対象コード(ケース2と同じ)

# a.py
from b import get_hoge
class TargetClass:
    def get_hoge2(self):
        return f"aaa_{get_hoge()}_bbb"
# b.py
from define import HOGE
def get_hoge():
    return HOGE # ここのHOGEを入れ替えたい
# define.py
HOGE = 'hoge'

ユニットテスト1(うまくいかないケース)

from a import TargetClass
import define

def test_hoge():
    define.HOGE = 'piyo' # 元々定義されている値を入れ替える
    target_class = TargetClass()
    assert target_class.get_hoge2() == 'aaa_piyo_bbb' # AssertionError

テストケースの from a import TargetClass ここで a.py が読まれて、from b import get_hoge が読まれて、b.pyfrom define import HOGE まで読まれる。 これにより、b内にHOGEが定義されてしまう。 これにより、テストケース内で define.HOGE を置き換えても、b.HOGE は既に定義されているため値が変更されない。

ユニットテスト2(うまくいくがイマイチなケース)

import define
define.HOGE = 'piyo' # テストモジュールの先頭でimportした直後に値を変更する
from a import TargetClass

def test_hoge():
    target_class = TargetClass()
    assert target_class.get_hoge2() == 'aaa_piyo_bbb'

うまく動作するには動作するが、linterに指摘されますし、実際に動かすときには1ファイルのテストだけではないと思われるので、先に実行される他のテストでbがimportされていたら結局同じ事が発生すると思われる。