PySparkでDataFrame.cacheはMEMORY_AND_DISKレベルキャッシュされる
概要
- タイトルが全て
- MEMORY_ONLYだと勘違いしていたためメモ
- persistも引数なしで呼び出すとMEMORY_AND_DISKなので同じ
※2021/12/17時点の最新版であるPySpark3.2.0の情報です
詳細
- レベルの変更の歴史
pyspark.sql.DataFrame.cache
がPySpark1.3.0で追加されたときは「MEMORY_ONLY_SER」レベル- 2.0.0で「MEMORY_ONLY」レベルに変更
- 2.1.0で「MEMORY_AND_DISK」レベルに変更
- ドキュメントに以下の記載があるのでScalaでは2.0から「MEMORY_AND_DISK」レベルだったようです
The default storage level has changed to MEMORY_AND_DISK to match Scala in 2.0.
- persistメソッドに指定できるレベルは以下を参照
Xiaomi Pad 5(MIUI12)を複数アカウント(ユーザー)で使用する
はじめに
- Xiaomi Pad5を購入したものの、デフォルトでは複数ユーザーで使用ができない?
- 端末というより、OSであるMIUIの仕様?
- とりあえず複数ユーザーで使用できるようになったので手順をメモする
- ただし、色々制限はかかっているの注意。詳細は下記参照
- 他にも良い手順があったらコメントでもTwitterでも良いので教えて下さい
わかる人向けの手順概要
※よくわからない人は下記にキャプチャ付きの詳細手順あります。
- 設定 → デバイス情報
- MIUIバージョンを10回タップ
- 開発者向けオプションを有効
- 追加設定 → 開発者向けオプション
- デフォルト値にリセット
- ※全然関係ない項目のように見えるがここが一番のハマりポイント
- 最下部のMIUI最適化をオフ
- 複数ユーザー → 追加
- 切り替え
- Google関連の設定はOFFにして初期設定をする
よくわかっていないこと(困っていること)
- ログイン時に追加したマルチユーザーでログインできない
- 所有者(管理者)でログイン後に、設定からユーザを切り替える必要がある
- Activity Launcher というアプリでユーザの切り替えのショートカットを作ることでとりあえずは対応
- 追加したユーザーの初期設定でインターネットに繋がらない
- Google関連の設定をONにして初期設定をするを行うと永遠に終わらない
詳細手順
- 設定 → デバイス情報 を表示
- MIUIバージョンを10回タップ
- 何回かタップしていると「デベロッパーになるまであとXステップです。」と表示される。
- 規定回タップすると「これでデベロッパーになりました!」と表示される。
- 追加設定内に「開発者向けオプション」が表示されているのでタップ
- 開発者向けオプション内のデフォルト値にリセットを複数回タップする
- ここからが本記事のキモ。私の環境だと「MIUIの最適化をオンにする」が表示されなかったので以下の操作を行う。
- 「自動入力の開発者向けオプションをリセットしました」と表示される。
- この設定は自動入力の設定なのでこの表示は正しい
- 何回かタップしていると「MIUIの最適化をオンにする」が表示されるのでオフに切り替える
- 重要な警告を熟読してから同意
- 複数ユーザーという項目が追加されているので選択
- オンに切り替え
- ユーザーまたはプロファイルを追加 → ユーザを追加する。
- 追加されたユーザをタップして切り替える
- ユーザの初期設定で「 Google関連の設定はOFF」にする
参考
GitHub ActionsでビルドしたドキュメントをGitHub Pagesで表示する
まとめ
- GitHub Pagesは「GitHub Enterprise Cloud 」プランの場合privateで使用する事ができる
企業でお金払っていてオンプレのGitHubでなければこのプランのはず- 追記: 有料プランでもTeamプランというのがありました。こちらではアクセス制御はできません
- GitHub ActionsでbuildしたドキュメントをGitHub Pagesに簡単に反映可能
- peaceiris/actions-gh-pagesというGitHub Actionsが便利すぎる
- ほぼ無料
はじめに
GitHub Pagesの存在は知っていてもprivateリポジトリで使用できないと思っている方は多いと思いますが、2021/01/21より「GitHub Enterprise Cloud 」であればprivateリポジトリで使用できるようになっています。(正確にはアクセス制御ができるようになった)
変更方法などは以下のドキュメントを参照
とは言え、GitHub Pagesの管理はそれなりに面倒です。 そこで、GitHub Actionsを併用する事でGitHubのイベントをトリガーにしてGitHub Pagesを簡単に更新する事ができたのでまとめておきます。
よくある問題
- CIでドキュメントをビルドしているが有効活用されない
- ビルドしたドキュメントを置く場所がない
- ビルドしたドキュメントにアクセスするのが面倒
手順
具体的なサンプル
- masterやdevelopブランチでhtmlを変更すると<ブランチ名.html>というファイル名でGitHub Pagesにコミットする
- https://github.com/yamap55/doc_github_pages_deploy
- GitHub Actionsの設定
- 業務では静的なhtmlではなくyamlから生成したhtmlをコミットするようにしている
- pytestのカバレッジをGitHub Pagesにコミットする
- https://github.com/yamap55/pytest_cov_github_pages
- GItHub Actionsの設定
- 本当はCoverallsやCodecov、Code Climate などのサービスを使いたいのだけど。。。
注意点
- privateリポジトリで使用する場合にはURLが「
http(s)://<username>.github.io/<repository>
」、「http(s)://<organization>.github.io/<repository>
」の形式ではない- GitHub Pages サイトの可視性を変更する - GitHub Docs
- ただ、ドメインを割り当てることができるらしい
参考
- 公式ドキュメントはかなり充実しています
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| +-------+---+
参考
- Shuffle operations
- join spark dataframes rows order
PySparkのDataFrameでは同名のカラムが許容される
概要
再現コード
作成時にカラム名が重複
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で定義されている。
で、どうすれば良いかというとスペースを入れることでエスケープを回避する。こちらについでも同じドキュメント内に記載がある。
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.py
の from 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されていたら結局同じ事が発生すると思われる。