英検準1級に合格した

英検準1級に合格した

2023年第3回試験
1月21日(日)1次試験
3月3日(日)2次試験
3月12日〜 合否発表(ネット上)
というわけで確認したら受かってました。

結果

特にリーディングは700/750って書いてあるから、CEFRのC1レベルに届いているな。 (準1級だとC1達成と判定しないので、結果ページのCEFRではB2ってことになっているが……)

B2の下限が英検CSEスコア2300、C1の下限が2600。俺の現在スコアが2487(下限+187点)。
ということはB2の中でも下から6割くらいのところか。

1次試験

筆記試験90分の時間配分をメモっていたのでここに書いておきます。最初の問題から順に解いています。

  • 語彙:14分
  • 大問2(設問31まで):8分(合計22分)
  • 大問3(設問41まで):30分(合計52分) 文章3つのそれぞれを解いたのが合計29分、40分、52分のとき。
  • 英作文開始。YES/NOそれぞれの理由を書き出して、NOで行こうと書き始めたのが16分後(合計68分)
  • 最初の書き出しにだいぶ悩んで、最後に書き終わったのは終了の2分前くらいだった。

ただリーディング大得意で読むの速い人の記録なので、もう少し長くかかると思います。 これから受ける人は英作文を早めに書き上げられると良いでしょう。

2次試験

さて2次試験について書いていくが……

英検の公式ホームページにも2次試験の過去問は掲載されていない。
第一に質問文を著作権の関係でも問題があるし、第二に正確な質問文は覚えていない。日本語でだいたいの意味を書いています。
(ただ2024年度から試験形式が変わるから、あまり参考にはならないかも?)

着席して軽く話をする。自己紹介は「35歳です、ITエンジニアとしてITコンサルティング会社で働いています」と喋った。
2問目で「休日は何をしていますか」と聞かれることに備えて、どう答えようか事前にあれこれ考えていたが、1問目だけで本題の試験に入った。

4コママンガは高層マンションの話。(詳しくは過去問題集を見てください。)

これを2分で説明する。 2コマ目では営業マンが「I can take you to the construction site.」的なセリフを喋っていた。 これを間接話法で話さなければいけないので、とっさには難しい。 「He told them that he can ... he could take them to the construction site.」と一度言い直した。

4コマ目は citizens are against the construction とか言った気がする。 反対するなら「be opposed to the construction」とか使えば良かったな。

ここから質疑応答が4問入る。1問目はお決まりの「4コマ目の登場人物の心情を答えよ」である。
……さて、後から振り返ると、これは英検準1級の過去の傾向、典型から外れた、結構イレギュラーな4コママンガである。
YouTubeの解説動画を見ると、1コマ目で何か課題が発生します的なことを言っていたと思うけど、今回の1コマ目では明らかに問題は発生していない。
4コマ目で問題が起きたといえば起きたが、そんなに重大な問題でもない。別に家を購入したわけじゃないんだから、

というわけで試験当時に戻ると、俺は困ってしまった。
「反対運動なんか知らない、俺は絶対にここに住むんだ」という極端な賛成も「反対運動が起きているから、ここに住むのは絶対に無理だ、諦めよう」という極端な反対も極端な反対も取りにくいな、と思った。
「ここに住むのは良いと思うが、住民の反対が強いので住むのが難しいのではないか……」的などっちつかずなことを言った。

第2問。 インターネット上のコンテンツは人々がものを買うことに影響しているか、じゃなかったっけ?
これはYESが答えやすいでしょう。

  • インターネット上には多くの広告がある
  • 人々は多くの広告を普段目にしているので、ものを買う上でそれらを参考にする
  • また口コミサイトも多くあるので、これもものを買う上で他の人の意見を参考にする

的なことを言おうとしたが、「参考にしている」が全然出てこなくてだいぶ詰まった。

第3問。 最近の若者が政治に関わっているかだね、確か。

なぜか咄嗟にグレタ・トゥーンベリが思い浮かんだので、YESで。

  • 今日は世界的な課題が多くある、例えば大気汚染や難民など。
  • これらの課題を解決するためには政治的な取り組みが必要である。
  • 若者はこれらの課題を解決すべきだと訴えている。また政治家にもアピールしている。

LOGOPHILIAの単語帳で見た「take to the streets」(街頭デモを行う)を使おうとしたが、間違って「take out to the streets」って言った気がする。

第4問。 都会に農業地帯を増やすべきか、だったと思う。

agricultural という単語が聞こえたので農地のことだとは思ったが、咄嗟に論説が組み立てられなくて、ただの緑地として答えてしまった。
都会の人間はストレスが溜まってるから緑を見てリラックスできるので、緑地……and agricultural areaを増やすべきだ、と最後で強引に軌道修正した。

これからどうする?

次は英検1級……だがそのハードルは結構高そう。

  • リーディング
    • の中の単語(語彙)。これはもう単語帳を頑張れということしかない。
      • 「でた単」のアプリやる? いやでもあのアプリ、準1級を少し使ったけど、単語の日本語の意味の書き方に引っかかることが多かったんだよな……
      • パス単は単語レベルが簡単になってしまった的な話を聞くので、ジャパンタイムズ社の問題集か、EXか、キクタンか……あれ結構種類があるな。
      • 過去記事で書いた通り、私は英単語をフレーズで覚えるのが大好きなので、これの1級のバージョンが出ないか期待している。
    • それ以外の読解力については心配してないです。英語を読むのは早い自信があるし、内容の把握もほぼ間違えないだろう。
  • リスニング
    • TOEICでやってたから余裕だろと思ったら今回ボコボコにされた。
    • 一発で聞き取って内容を把握する
  • ライティング
    • 要対策だと思うけど
  • スピーキング
    • 一番苦手……
    • やはりこれは日常生活の中で英語を喋る機会がないと言うことに尽きる。なので機会を作らなければならない。
    • 準1級の前はDMM英会話をやったけど、少しやったら飽きてしまった。

英検1級に本当に受かりたかったら全体的に底上げが必要だと思うけど、なんとなく単語帳だけやってしまって試験の日を迎えるような気がしてならない。

ちなみに準1級の過去問は↓を使っていました。

それでは。

2023年に買ってよかったもの

2023年に買ってよかったもののメモ。あんまり多くない気がする。

Shokz OpenRun Pro 骨伝導イヤホン

一番はこれだろう。7月のAmazon大規模セールにつられて購入。
今まで有線のイヤホンでリモートワークの会議に参加していたが、パソコンの前にいる必要がなく自由に動けるようになったのは良い。音質も聞きやすい。口の前にマイクはないのだが、ちゃんと話もできる。
ジョギングとの相性も良い(耳をふさがないので、車の音なども聞こえる)。
ただ寝転んだ場合の相性は悪い(頭の後ろが当たるので)。

……そういえば有線で良いイヤホン買ってないな……(Ultimate Ears UE900sは断線で壊れてしまった。あとゼンハイザーのIE60はここ数年間行方不明だ。どこ行ったんだろう……)

VOLTRX 電動プロテインシェイカ

プロテインを飲み始めたときに、自分で混ぜなければいけないのに面倒くさくなって電動シェイカーを購入。
(自分で振るシェイカーをすっ飛ばして、いきなり電動シェイカーを買った)
ボタンを押すだけなので楽。プロテインの粉がきれいに溶ける。
ただ、物持ちは悪い。2022年末に購入して、数ヶ月したらボタンを押さなくても勝手にスイッチがオンになって「ブーン」と音を立てるようになってしまった。その後、最近は完全に壊れて電動部分が全く動かなくなった。捨てて次を買おう……
考えてみると、これはコップの一種なので水を入れたり洗ったりして濡れることが多い。そして、充電式なのでUSB Type-Cで充電する必要がある。当然、ちゃんと乾かしてから充電するようにすれば問題ないのだろう。しかし、面倒くさがりの俺には無理だった……

VALX プロテイン

プロテインの方はもっぱらVALXを買っている。
公式サイトで毎月末にセールしているので、無くなってきたら4つセットで買うことにしている。
最初に買ったGronGがあまりにもまずかったので、色々調べたりしてここにたどり着いた。
個人的にはどの味を買っても美味しくてハズレがない。
と思うが、動画とかを見ているとまずいって言ってる人もいる模様。個人によって感じ方に差があるようなので自分にあったプロテインを見つけるしかないね。

最初に飲むならカフェオレ味が無難な気がする。プロテインっぽさが無くて普通のカフェオレと思って飲める。ロイヤルミルクティーとか抹茶も好き。

ランニングポーチ

ランニングするときにポケットにケータイ入れてると揺れて落ちそうで怖いな……ということで購入。
スマホと鍵とSuicaを入れる。

SUZURI ドライTシャツ

犬たくさん バックプリント
DDRするときはだいたいこれ。汗でびしょ濡れになってもすぐに乾く。
面白いデザインのドライTシャツ出てこないかな。

ワークマン ボアフリースカーディガン

最後は貧乏くさいやつだが……
サンシャイン池袋に「ワークマン女子」があって、出かけたついでに寄ってみたらフリースが980円でびっくりしたので買ってしまった。
これが1000円を切るってどうなってるんだ。ワークマン恐るべし。家で着ているが、暖かくて良い。
商品ページが見つからなかったので、紹介記事の方でリンクを張っておく。
ワークマンで買える「ボアフリースカーディガン」が、想像以上の快適さだった。コレで980円はさすがすぎる…

総評:やっぱり少ないな

ちゃんと不便や必要性を察知して「これを買おう」と決める力が弱い気がする。
面倒くさがりだからそういう意思決定を避けてしまうのであった。
年間の収支は結構なプラスだったし(マネーフォワードの記録を見た)、もっと積極的に新しいガジェットとかを買ってもいいと思うんだよな。 毎月1万円とか5千円を新しいものを買うのに強制的に充てるとかしたほうが良いのか……
しかし問題は何を買うかよな。例えばYouTubeで「買ってよかったもの」で検索するとたくさん出てくるけど、それが俺に合うかは分からないわけだし。
さてどうしたものか……みんなどうしてるんやろ?
それでは。

2023年の振り返り

2023年振り返り

1月も気づけばあっという間に半分が終了してしまった。
忘れないうちに2023年の振り返りをしよう。

2022年版はこちら。
2022年の振り返り - 子供の落書き帳 Renaissance

※ 今年・去年・来年がややこしくなりそうなので、2023年・2024年で表記を統一します。

仕事

2023年はずっと1つのプロジェクトに従事していた。 12月にリリース・本番稼働ができて、良かったね……と、少なくとも表面的には言えるだろう。 だが本当にうまくやれただろうか。

リリース・本番稼働に向けた各段階において、起こり得る問題を先回りして特定し、関係する他のチームの人たちと適宜相談して課題を解決できた。
色々なところから飛んできた調査依頼や相談については、丁寧に応対してきたし、それによってプロジェクトが前に進むことに貢献できたという自負はある。

じゃあ、何で自分のところの仕様に詳しいのかと考えると、何というか「そのチームに長く在籍しているから」なのだ。 そりゃ長いことその部分に関与してきて、ずっとやっていれば詳しくもなるだろうという当然の道理である。
ちゃんと自分の理解を、他のチームにも分かるようにドキュメントや仕様書として残すべきだったのだが、 納期に間に合わせるためにドキュメント化は犠牲になったのである。 去年の仕事の心残りは、ドキュメントがボロボロだったこと、この1点に尽きる。
自分の強みは、複雑な事柄を読み解いて、他の人にも分かるような適切な形で整理することだと思っている。 その強みがフロー(日々のSlackのやり取り)では活きたものの、永続的なストック(各種ドキュメント)に強みを活かすことが少なかったのは、まぁまぁフラストレーションになっている。

他チームが(俺のチームの担当部分を含む全体の仕様を整理するために)俺のチームの範囲の動作仕様を整理した図を作っていたのとか、恥でしょ。 我々が職務怠慢をしていて他のチームに肩代わりしてもらったことの証左なんだから。今思い出したって忸怩たる気分だ。 というか、ドキュメント不足のせいでどういう仕様なのか俺自身でさえ把握できていない箇所があるしな。

Pythonのコードを自分自身が書くことは殆ど無くなって(少しはある)、人のコードをレビューするのが多くなった。
じゃあプロジェクトマネジメントやチームリーディングを担当しているかというと、そういうわけでも無いし、自分の担当している業務がうまく表現できなくて、いつももどかしい気分になる。
自チームで作っているシステムについて何でもやります、というのが正しい気がしている。

一人暮らし

面倒くさがりな性格のせいで、「一応生きていくことはできるが、使いにくくて不便」という状況になっているので、どうやって住みやすい暮らしにすれば良いかな……

一人暮らしして1年半、この状態が継続中……生活力が皆無であることが露呈するのであんまり具体的には言えないけど。
あと、一人暮らしを脱出できなかったわ。

自己学習

2023年も2022年に続いてほとんどやらなかったなぁ……。

ブログに技術的な内容で書いたのは、この2個か。

確かにこの2つは実際の業務で詰まったから、調べてまとめたんだよな。今でも思い返すことができる。
他の点は行き詰まることが無かったのか……というと、そういう訳では無い。
課題は他にもまだまだあったが、課題の抽象度が上がって、上記2つのような具体的なものとして指し示すことができなくなった。
こにふぁーさんのKonifar's ZATSU みたいな感じで、うまく抽象化して書くのが良いのかなぁ……

connpassのイベントの話

年末に、同僚が「connpassなどの外部の勉強会に積極的に参加している」という話をしていた。 まず第一に「勉強熱心ですごいなぁ」と思ったが、その次に「もっと早く知りたかったなぁ」と思った。 (別に個々のイベントの内容を全部知りたいわけではなくて、(俺を含む)周りの人を感化することができたんじゃないか、的な。)

会社内の周囲の人たちが、(仕事以外に)どういう技術的な活動をしていて、どういうことに興味があるか、が見えにくい。
俺、チーム、プロジェクト、顧客、以上。って感じだわ。

さりとて、どのイベントに行くかって考えると、難しくないかなぁ。 ディープラーニングを業務で使っているわけでもないし。機械学習じゃないデータ分析系ってあんまりイベントにならない気がするし。 ん〜〜、Pythonの書き方とかシステム構築の一般論とかそのへんだろうか。

英語だけはやり始めた話

11月あたりから急に英語をやりだした。
きっかけはおそらく、ふるやん(@furuya1223)さんが漢検1級を取ったことだと思う。 https://www.creativ.xyz/kanken-1k/

(俺自身が漢検準1級を10年ほど前に取ったので分かるのだが)漢検1級は準1級とは別格の難しさがある。 その割に取っても使い所が殆ど無いという資格である。 あれを1年くらい? それ以上? 長期間にわたって継続して勉強して合格まで行くの、マジすごい。と思った。

俺も何か資格を取るかなーとぼんやり考え始めた。 合格してどれくらい役に立つ/役に立たない目標にしようか色々考えつつ、漢検1級は無理な気がしたので英語にした。 英語使えたほうが何かと便利だし……(既に役に立つ方を目指そうとしてしまっている)
TOEIC満点を目指すのは、意味があんまりない資格なのでそれを目標にしようかな(既に一度975点を取っているので、990点とそう変わらないため)とも思った。 結局、TOEICと別の尺度でやってみたら面白そうな気がしたので、英検で。 ライティングとスピーキングなんて普段全くやらないから、これを機にやるのも悪くないだろう。 2024/1/21に英検準1級(1次)を受けます。準1級の後、英検1級を目指すか、TOEIC満点を目指すかは考え中。

今は英字新聞を読んだり、YouTubeで英語のニュースを聞いたりして勉強しているが、 ちょっと前に勉強した単語が別のところで登場しているのを見たりすると、純粋に楽しくて嬉しい。
やっぱり自分は(ストレングスファインダーでいうところの)学習欲の人間なんだなーと。
「学習欲」が強い人は、すぐに役立ったりしなくても、何かを学ぶときに楽しくなる、的なことが書いてあった気がする。

ゲーム

音ゲー

2023年にプレイした音ゲーは、ほとんどがDDRだった。
skill attack 2022末
skill attack 2023末

レベル14の99万点が1→3譜面、レベル15の99万点が0→1譜面。レベル16の95万点が1→12譜面。 まぁ逆詐称の譜面が新たに増えたという理由も一部あるとはいえ、地力は向上したと思っていいだろうなぁ。

あとはクリア目線、18を弱・中・強の3つに分けたときに弱は大体はクリアできて、中はできたりできなかったり、 クリアできそうでできないのが、Cosy Catastrophe、VOLAQUAS、Triple Journey -TAG EDITION-、このあたり。 というか他の曲はほとんどやってないな。頑張ってクリアするところまで粘着してない。

んー……一応ドラマニも書いておく? でも全然プレイして無かったと思う。
HIGH-VOLTAGE gsv記録
FUZZ-UP gsv記録
新曲を全然詰めてないな。あと旧曲もちゃんとやってないからスキルポイント減ってるな。オワタ。

音ゲー以外

原神が主。
螺旋12層が全然できないのが悩み。多分適当にやっているから戦闘スキルが低い。

ポエム

最近、ぼんやりと「もう少しあの活動に時間と労力をかけていれば、もっと結果を出せていたのかもしれないな」と思うことが多い。 簡単に言えば時間配分のトレードオフというやつだ。
「あの活動」に入るのは、だいたいちょっと取り組んだこと、多分音ゲーDDR)とAI画像生成(Stable Diffusion他)である。
技術的な取り組みが「あの活動」に代入されることがない時点でどうなのよ感がある。結構、ある。技術へのコミットメントを増やしたいという課題意識を、そもそも持っていないということなのか……???
一方で、去年1秒もやらなかったこと、例えば「もう少し将棋に時間をかけて取り組んでいれば……」というふうに考えたことはないな。別に将棋のことは何とも思っていないので。

音ゲーDDR)が分かりやすくて、去年はDDRに集中していたのでDDRの腕前は伸びたけど、その代償に他の音ゲードラマニとサンボル)は腕が落ちた。なぜならプレイしていないので。
それでもDDRのプレイ頻度はそんなに高くないから、もっとプレイ回数を増やせばもっとうまくなるだろうけど、そうすると他の活動の時間を減らさなければならない。
まぁ労力を投下したからと言って結果が必ず良くなるとは限らないな。 極端な場合、時間と労力をかけたのに何の成果も得られなかった場合もあるから……(遠い目)

……と言いつつ、ダラダラとゲームしてたら休日が終わったりするんだよな。「ある活動を取って他の活動を捨てる」とかじゃなくて、「何の足しにもならない活動」に時間を割き過ぎている場合がある。
などと思っていたら、こんな動画がYouTubeのレコメンドに上がってきていた。やってみようか? Use Strategic Thinking to Create the Life You Want - YouTube

あと英語は別に「頑張って歯を食いしばって英語をやっている(=努力して英語学習に時間と労力を費やしている)」わけじゃないんだよな。
何か面白いから英単語帳を開くし、英字新聞読むし、Ankiで単語帳を進める、とかやってると、気づいたら1日30分とか1時間くらいは英語関連のことをやっている。
忙しいふりしてたけど、普通に英語学習の時間が割り込んでくる余地あるんじゃん! と自分で自分に驚いているのが1つ。
あとは、こういう風に気づいたら時間をかけて取り組んでいてスキルが上がるならそれが一番いいよね、というのが1つ。

まぁ子育てしてる人からすれば「独り身のくせに(=自分の時間と財産を全て自分のためだけに使えるくせに)何言ってるんだ」とか言われそうだが。

特に結論はない。強いて言うならば

  • 自分自身がどの活動にどの程度、時間と労力を費やしているのかを把握したい
  • 「何の足しにもならない活動」に時間を費やすのをやめたい
  • こと技術に関しては、「気づいたら時間と労力を費やしていた」になるような仕組み(仕掛け)を作れないかな

あたりだろうか。

総評?

だいたい書きたいことは書けたので完成ということにする。

ひとまず以上です。

今までの英語学習と試験結果を振り返る

長文のブログを書く体力と習慣がなくなってしまった……

最近になってなぜか英語勉強の熱が再燃してきた。
英検準1級の試験に申し込みしてきた。これを期に今までの英語学習を振り返るための記事である。

小学校より前

セサミストリートとか見てたと思うけど、詳しいことはもはや覚えていない。

小学校

英語の勉強らしい勉強をしたのは、小6の最後で中学受験に合格したあとのこと。3日間か5日間だったような。
いわゆるフォニックス(英単語の発音の規則)について教わった。

中学校

Asahi Weekly

朝日新聞から発刊されている(英語学習者向けの)英字新聞。多分母親がこの新聞を取ったんだと思う。暇なときに読む、くらいだったけど、読解力の役には立ったと思う。
そういえば、思い出した。中2のときの先生が「英語の文章をノートに書き写して。写したページ数に応じて得点を与える」という課題を出したことがある。
で、俺はこのAsahi Weeklyの記事の文章を書き写して提出した。ノートが返却されたときに「君は難しい文章を書いてくれたので、通常の得点の2倍で計算しておきます」という注釈が書いてあって、嬉しかったな。
むしろ、英字新聞のような書き写すネタが無かった他の人は、何の文章を書いていたんだろうか? 教科書や問題集の文面を書き写すだけだと、飽きると思うけど。

K会

河合塾系列の塾。夏期講習などの長期休みだけ参加。
「英語を学ぶんじゃなくて、英語で学ぶんや!」みたいなコンセプトで、聖書とかギリシャ神話とか、あとはイギリスの歴史とか科学史とか、色々な長文を読まされた気がする。何ページも続く英文に抵抗がなくなったのはK会のおかげかもしれない。

PEANUTS / スヌーピー

小学校の頃からスヌーピーの漫画が好きでよく読んでいたが、中学校に上がって英語で読めるようになると日本語訳と見比べながら読むようになったかな。
海外の文学作品とかだったら、日本語版には日本語だけが書いてあるけど、PEANUTSはなぜか昔から原文と和訳を併記する形だった。
(あの独特の書き文字も漫画作品の一部だからかなぁ。日本語版の形式にもよるが、セリフの中の英文はそのままで、枠の外に日本語訳が書いてあることが多い。)
偶然だけど、好きになった作品が英語の勉強に役立つもので良かった。

高校

システム英単語

大学受験のときの英単語帳はシステム英単語の1冊だけだった。単語はフレーズで覚えると良い、という序文に感銘を受けたことを覚えている。(最新版のシステム英単語だと序文は違っている。当時の版のもの。)
この教えに従い、現在に至るまで、単語はフレーズで覚えたい派である。
しかしフレーズで覚えられるという条件を満たす、システム英単語より上のレベルの単語帳を見つけていない……
誰か「フレーズで覚えるという、システム英単語と同形式の単語帳をまとめてレビュー」って記事書いてくれないかな。

受験当時に使っていたのはこの版。 https://www.amazon.co.jp/gp/product/4796110593/

Z会東大マスターコース

それと、塾はZ会に通っていた。この記事の中で唯一個人名が出てくるが、柳瀬晃先生である。
まぁなかなか厳しい先生だった。自作プリントを作って配ることが多く、そのプリント演習でガチで難しい構文の英文和訳を解かされた。その甲斐あって、英文解釈の力はめちゃめちゃに強くなった。自作のプリントは捨てるのも惜しいので未だに取っておいてある。
「国外留学したこともない俺が英語力で困ることもなくやっていけているのは何故か」といえば、柳瀬先生のおかげであることは間違いない。15年以上経った今でも、いくつかの話は思い出せるもんな……
「はい、『at the ... of 〜』で『〜を代償・犠牲にして』の意味になる単語4つを答えてもらおうか、では〇〇さんどうぞ」(正解は cost/price/expense/sacrifice)

柳瀬先生は「アラン・ド・ボトン」という小説家が好きらしく、この人の文章から自作プリントを作ることが非常に多い。そういや、この「アラン・ド・ボトン」の小説をいつか読もうと思って、まだ読んでないな……

(柳瀬先生の評判を知りたくてこの記事を読む東大受験生はいないと思うが、もしいたら1点だけ注意。英作文については柳瀬先生は全部カットするので、自分でちゃんと対策しましょう。自分でやらなかった俺は試験本番で英作文の2問中1問を空欄で提出してしまった。まぁ、それでも受かったけど。)

大学〜大学院

TOEIC(2007年?月)

1回公式問題集を解いただけで受験。確か920点。
すっかり拍子抜けした俺は「はーん、TOEICなんて簡単じゃねーか」という気持ちになった。

TOEFL(2010年5月)

大学院の入試で、英語の試験の代わりにTOEFL iBTのスコアを提出する必要があったので受けた。
これは手元に記録がある。 120点満点で、Reading 27, Listening 22, Speaking 14, Writing 20で83点。 Reading > Listening > Writing > Speaking という大小関係なのは納得である。Speakingは普段全然やってないから、できる気がしないからな。

TOEIC(2011年11月)

M1の11月なので、就活のためにもう一度受けとくかと思って受けたんだろう。 これは手元に記録がある。総合975(Listening 495, Reading 480)。 絶対リーディングのほうが自信があったのに、リスニングが満点でリーディングが満点-15というのが意外。

社会人

TOEIC (2013年4月)

新卒で入った会社で、業務後に受けさせられたやつ。なので団体試験。(TOEIC IPテスト) 確か905点。

Anki

単語帳アプリ。出題と復習までの間隔をうまく調整してくれる。PCとスマホで同期が取れる。そういう特徴についてはここで説明する気はないので他を見てほしいが、オススメのアプリである。
Ankiに入れた単語帳は英語だけじゃなくて、資格試験(応用情報/LPIC/セキュスペ)で覚えたいものとか、その他IT関連で覚えておきたいものを適宜入れていた。前職では社員食堂があったので、そこで並ぶときか昼飯を食べながらでAnkiをやることが多かった。
ただ、何年かログインしていないとデータが消える。最近(2023年12月)にAnkiを再度使おうと思って、ログインしたら消えていた。

Ankiを使う場合、自分で問題を作るか、既存の(公開されている)デッキを使うかのどちらかになる。 資格試験の途中などで、覚えたいものが出てきた場合は当然前者だが、ここで大きな欠点がある。自分で問題を作って入力するのがめちゃめちゃ時間がかかるのだ。 休日にカードを作り始めたら1〜2時間経過していたことも結構あったような覚えがある。

単語のリストがあって(たとえばSVLとか)、1000個の単語とその訳を覚えたい、とかであればある程度自動的にできるかもしれない。……が、俺は普段、覚えたい単語に出会ったときは以下のステップを踏む。

  • ネットの英辞郎を引く
  • 電子辞書の例文一覧で単語を検索する
  • 2つの中から良さそうな例文を選ぶ
  • 単語帳に入力する

なので時間もかかるし自動化もし辛い。悩ましいところである。

語彙力高めの日-英のデッキは少ないので、 SATとかGRE向けの、英英の(日本語訳の無い)デッキを使ってた。 ……という話が旧ブログに書いてあるな。

http://luvtome.blog5.fc2.com/blog-entry-593.html
http://luvtome.blog5.fc2.com/blog-entry-600.html
http://luvtome.blog5.fc2.com/blog-entry-613.html

TestYourVocab

2023年12月23日の結果

(2023年12月23日時点) 書くついでにまた測ったら9201と言われた。 10000まで行きたい〜と思いつつ、あと800覚えるのなかなか大変だなぁ。うへ。

英字新聞

ProPublicaのTwitterアカウントをフォローしている。何のきっかけで知ったのか忘れたけど。 ProPublica、New York Timesはたまに読んでた。……久々にNew York Timesを再訪問したら、有料会員にならないと全然記事が読めなくなっていてガッカリした。 最近はGuardianが無料で全部読めるということに気づいたので、たまに読んでいる。


何か書き加えることができたら加筆します。
それでは。

IIJmioの料金プランをギガプランに変更した

IIJmioの料金プランを2023年6月18日に、昔のライトスタートプラン(データ6GB)→新しいギガプラン(データ5GB)に変更したという話。
請求額は税込み2700円〜3000円から1000円程度に減った。もっと早くに変えておけばよかったぜ……!

プラン変更前の状況

マイページから確認すると下記のとおりだった。

申し込み日:2016年10月30日
料金プラン:ライトスタートプラン

毎月の料金は、
月額基本料(ライトスタートプラン) 税抜1520円、税込1672円
音声通話機能付帯料 税抜700円、税込770円
で合計税込2442円。
これに通話代が数百円かかって、月に2700円〜3000円くらい払っている。

倉田けい さんのIIJmioの広告がTwitterで流れてきて、俺は重い腰を上げたのであった。

変更先のギガプランを決める

ライトスタートプランは、データが毎月6GB使えるプランである。
https://www.itmedia.co.jp/mobile/articles/2302/22/news203.html によれば、 4GBのプランが2023年2月に5GBに増量された。 ライトスタートプランの6GBから移行するなら、これが良いだろう。

過去30日間のデータ利用量を確認したら(正直ここはもっと前までデータ利用量を確認させてほしい)、ちょうど6GBちょっとだった。
ただこれは1泊旅行に行ったときに2GB以上使っている影響が大きい。普段は4GBでギリギリ、5GBなら余裕を持って使える、というところか。5GBに上がってくれて助かった。

と思ったらどうやらギガプランの場合は月ごとのデータ利用量を照会できるらしい。
https://help.iijmio.jp/answer/611e5819ae05cd001c0ec994?search=true
プラン切替後に確認したが、 ギガプランを使っている時期だけでなく旧プランのときも含めて 過去1年分の月ごとのデータ利用量を確認できる。

(余談:itmediaの記事中の資料によればギガプランの人が9割近くいるらしい。俺完全にラガードじゃん。)

プランの「データを分け合う方式」の違い

https://www.iijmio.jp/hdc/spec/index.html#comparison の比較にある通りで、データを分け合う方式が異なる。

新しいギガプラン = プラン”間”でデータを分け合う
例)2ギガプラン、5ギガプランをそれぞれ契約。合計7ギガを2人で分け合う。
従来のライトスタートプランなど = プラン”内”でデータを分け合う
例)ファミリーシェアプラン(12GB)内でSIMカード3枚でデータ量を分け合う。
https://www.iijmio.jp/hdc/spec/index.html#comparison

俺のiPad Proは、SIMを差せるのに差さずに使い続けるという変な使い方をしている。
ギガプランの場合、もしiPadにSIMを入れたくなったら、新しく別のギガプランを契約する必要がある。SIMが2枚必要ならば契約プランも2つ必要だからだ。
この点がずっと引っかかっていて躊躇っていたけど、いまはSIM無しで使っているので、SIMを入れたくなったら考えることにしよう……。

プラン変更時の注意点

重要説明事項からコピー。

IIJmioモバイルサービス(ギガプラン)へ変更された場合、元のプラン(IIJmioモバイルサービスまたはIIJmioモバイルプラスサービス)へは戻すことはできません。
IIJmioモバイルサービスまたはIIJmioモバイルプラスサービスでご利用中のクーポン残量を引き継ぐことはできません。残量は消滅します。

ずっと溜まっていたデータ通信量の残量は消えてしまうので注意。

プラン変更後の状況

6月中旬に変更したので、7月1日から適用になった。

データについて。上記の通り、変更のタイミングでクーポン残量が無くなったが、7月は5GBのうち4.7GBを使った。この調子なら少しずつ溜まるので大丈夫だろう。

料金について。 まずギガプランの5GBは税抜900円、税込990円だ。
2023年7月利用分の請求金額には、6月の電話の通話料が入る。(電話の通話料に関しては1ヶ月ズレるらしい)6月はたまたま電話を使わなかったので、合計の支払い料は1000円を切った。 マジか……ネットで完結する手続きするだけで月間1700円くらい浮いたぞ……もっと早くに変えておけばよかったぜ……!

それでは。

daskのquery関数で変数名を指定する方法

daskでデータ絞り込みをするためにquery関数を使ったけど、構文が難しくてちょっと詰まった話。

daskのDataFrameに対するquery関数の公式ドキュメントはこちらだ。
dask.dataframe.DataFrame.query

このドキュメントを見ると、
「pandasは@で変数名を使えるが、daskではそれは使えないので、代わりにf文字列かlocal_dictキーワードを使ってくれ」と書いてある。
なるほど、@の代わりにf文字列を使えばそれで良いのね。……と単純に考えていると、ちょっとつまずく。という話である。

準備

import pandas as pd
import dask.dataframe as dd
import dask
pd.options.display.notebook_repr_html = False  # jupyter notebook上での出力形式を制御するために書いています。無くても動きます。
# 動作環境の確認
print(pd.__version__)
print(dask.__version__)

# --------------------

1.1.2
2023.1.0

サンプルデータの作成

# データに特に意味はない。https://linus-mk.hatenablog.com/entry/pandas-unique-integer-id から持ってきて適宜改変。
df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'Charlie', 'Alice', 'Bob'],
    'item' : ['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff'],
    'number'    : [3, 2, 4, 3, 2, 1],
    'id_code' : ['012', '123', '234', '123', '012', '345']
})
df

# --------------------

      name item  number id_code
0    Alice  aaa       3     012
1      Bob  bbb       2     123
2  Charlie  ccc       4     234
3  Charlie  ddd       3     123
4    Alice  eee       2     012
5      Bob  fff       1     345
df.dtypes

# --------------------

name       object
item       object
number      int64
id_code    object
dtype: object
ddf = dd.from_pandas(df)

# --------------------

---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    <ipython-input-6-bcb6b963da62> in <module>
    ----> 1 ddf = dd.from_pandas(df)
    
    /usr/local/lib/python3.8/site-packages/dask/dataframe/io/io.py in from_pandas(data, npartitions, chunksize, sort, name)
        260 
        261     if (npartitions is None) == (chunksize is None):
    --> 262         raise ValueError("Exactly one of npartitions and chunksize must be specified.")
        263 
        264     nrows = len(data)
    ValueError: Exactly one of npartitions and chunksize must be specified.

どうもよく分かっていないのだが、dask.dataframe.from_pandasはnpartitionsとchunksizeのうちどちらか片方(のみ)を指定する必要があるらしい。 今は特に何でも良いので、npartitions=1を指定する。

ddf = dd.from_pandas(df, npartitions=1)
print(ddf)

# --------------------

Dask DataFrame Structure:
                 name    item number id_code
npartitions=1                               
0              object  object  int64  object
5                 ...     ...    ...     ...
Dask Name: from_pandas, 1 graph layer
ddf.compute()

# --------------------

      name item  number id_code
0    Alice  aaa       3     012
1      Bob  bbb       2     123
2  Charlie  ccc       4     234
3  Charlie  ddd       3     123
4    Alice  eee       2     012
5      Bob  fff       1     345

これで準備はできた。

数値型のカラムの場合

上述の公式ドキュメントにも載っている、数字の例を見てみよう。
まず、データのうち、numberカラムが2であるものを抽出しよう。

# pandas 直接値を指定
df.query("number==2")

# --------------------

    name item  number id_code
1    Bob  bbb       2     123
4  Alice  eee       2     012
# dask 直接値を指定
ddf.query("number==2").compute()

# --------------------

    name item  number id_code
1    Bob  bbb       2     123
4  Alice  eee       2     012
# pandas 変数名を使用 @
num = 2
df.query(f"number==@num")

# --------------------

    name item  number id_code
1    Bob  bbb       2     123
4  Alice  eee       2     012
# dask 変数名を使用 f文字列、成功
num = 2
ddf.query(f"number=={num}").compute()

# --------------------

    name item  number id_code
1    Bob  bbb       2     123
4  Alice  eee       2     012
# pandas 変数名を使用 実はf文字列でも行ける
num = 2
df.query(f"number=={num}")

# --------------------

    name item  number id_code
1    Bob  bbb       2     123
4  Alice  eee       2     012

pandas側で@variable_nameと書く代わりに、daskでは{variable_name}と書けば良さそうな気がしてくる。 ところがそれが上手く行かないケースが存在するのだ。

文字列型のカラムの場合

データのうち、nameカラムが"Bob"であるものを抽出しよう。

# pandas 直接値を指定
df.query("name=='Bob'")

# --------------------

  name item  number id_code
1  Bob  bbb       2     123
5  Bob  fff       1     345
# dask 直接値を指定
ddf.query("name=='Bob'").compute()

# --------------------

  name item  number id_code
1  Bob  bbb       2     123
5  Bob  fff       1     345

ここまでは何も問題ない。
ところが、変数名を使用すると状況が変わってくる。

# pandas 変数名を使用 @
target = 'Bob'
df.query(f"name==@target")

# --------------------

  name item  number id_code
1  Bob  bbb       2     123
5  Bob  fff       1     345

pandas側で@variable_nameと書く代わりに、daskでは{variable_name}と書くと失敗する。

# dask 変数名を使用 f文字列 失敗例
target = 'Bob'
ddf.query(f"name=={target}").compute()

# --------------------

エラー。長いので折りたたみます。

クリックでエラー内容を表示

    KeyError                                  Traceback (most recent call last)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/scope.py in resolve(self, key, is_local)
        187             if self.has_resolvers:
    --> 188                 return self.resolvers[key]
        189 
    /usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/collections/__init__.py in __getitem__(self, key)
        897                 pass
    --> 898         return self.__missing__(key)            # support subclasses that define __missing__
        899 
    /usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/collections/__init__.py in __missing__(self, key)
        889     def __missing__(self, key):
    --> 890         raise KeyError(key)
        891 
    KeyError: 'Bob'
    
    During handling of the above exception, another exception occurred:
    KeyError                                  Traceback (most recent call last)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/scope.py in resolve(self, key, is_local)
        198                 # e.g., df[df > 0]
    --> 199                 return self.temps[key]
        200             except KeyError as err:
    KeyError: 'Bob'
    
    The above exception was the direct cause of the following exception:
    UndefinedVariableError                    Traceback (most recent call last)
    /usr/local/lib/python3.8/site-packages/dask/dataframe/utils.py in raise_on_meta_error(funcname, udf)
        194     try:
    --> 195         yield
        196     except Exception as e:
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in _emulate(func, udf, *args, **kwargs)
       6570     with raise_on_meta_error(funcname(func), udf=udf), check_numeric_only_deprecation():
    -> 6571         return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))
       6572 
    /usr/local/lib/python3.8/site-packages/dask/utils.py in __call__(self, _methodcaller__obj, *args, **kwargs)
       1102     def __call__(self, __obj, *args, **kwargs):
    -> 1103         return getattr(__obj, self.method)(*args, **kwargs)
       1104 
    /usr/local/lib/python3.8/site-packages/pandas/core/frame.py in query(self, expr, inplace, **kwargs)
       3339         kwargs["target"] = None
    -> 3340         res = self.eval(expr, **kwargs)
       3341 
    /usr/local/lib/python3.8/site-packages/pandas/core/frame.py in eval(self, expr, inplace, **kwargs)
       3469 
    -> 3470         return _eval(expr, inplace=inplace, **kwargs)
       3471 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/eval.py in eval(expr, parser, engine, truediv, local_dict, global_dict, resolvers, level, target, inplace)
        340 
    --> 341         parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
        342 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in __init__(self, expr, engine, parser, env, level)
        786         self._visitor = _parsers[parser](self.env, self.engine, self.parser)
    --> 787         self.terms = self.parse()
        788 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in parse(self)
        805         """
    --> 806         return self._visitor.visit(self.expr)
        807 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Module(self, node, **kwargs)
        403         expr = node.body[0]
    --> 404         return self.visit(expr, **kwargs)
        405 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Expr(self, node, **kwargs)
        406     def visit_Expr(self, node, **kwargs):
    --> 407         return self.visit(node.value, **kwargs)
        408 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Compare(self, node, **kwargs)
        698             binop = ast.BinOp(op=op, left=node.left, right=comps[0])
    --> 699             return self.visit(binop)
        700 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_BinOp(self, node, **kwargs)
        519     def visit_BinOp(self, node, **kwargs):
    --> 520         op, op_class, left, right = self._maybe_transform_eq_ne(node)
        521         left, right = self._maybe_downcast_constants(left, right)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in _maybe_transform_eq_ne(self, node, left, right)
        440         if right is None:
    --> 441             right = self.visit(node.right, side="right")
        442         op, op_class, left, right = self._rewrite_membership_op(node, left, right)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Name(self, node, **kwargs)
        532     def visit_Name(self, node, **kwargs):
    --> 533         return self.term_type(node.id, self.env, **kwargs)
        534 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/ops.py in __init__(self, name, env, side, encoding)
         83         self.is_local = tname.startswith(_LOCAL_TAG) or tname in _DEFAULT_GLOBALS
    ---> 84         self._value = self._resolve_name()
         85         self.encoding = encoding
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/ops.py in _resolve_name(self)
        100     def _resolve_name(self):
    --> 101         res = self.env.resolve(self.local_name, is_local=self.is_local)
        102         self.update(res)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/scope.py in resolve(self, key, is_local)
        203 
    --> 204                 raise UndefinedVariableError(key, is_local) from err
        205 
    UndefinedVariableError: name 'Bob' is not defined
    
    The above exception was the direct cause of the following exception:
    ValueError                                Traceback (most recent call last)
    <ipython-input-17-6b73727f207c> in <module>
          1 # dask 変数名を使用 f文字列 失敗例
          2 target = 'Bob'
    ----> 3 ddf.query(f"name=={target}").compute()
    
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in query(self, expr, **kwargs)
       5178         2  1  3    2
       5179         """
    -> 5180         return self.map_partitions(M.query, expr, **kwargs)
       5181 
       5182     @derived_from(pd.DataFrame)
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in map_partitions(self, func, *args, **kwargs)
        873         None as the division.
        874         """
    --> 875         return map_partitions(func, self, *args, **kwargs)
        876 
        877     @insert_meta_param_description(pad=12)
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in map_partitions(func, meta, enforce_metadata, transform_divisions, align_dataframes, *args, **kwargs)
       6639     dfs = [df for df in args if isinstance(df, _Frame)]
       6640 
    -> 6641     meta = _get_meta_map_partitions(args, dfs, func, kwargs, meta, parent_meta)
       6642     if all(isinstance(arg, Scalar) for arg in args):
       6643         layer = {
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in _get_meta_map_partitions(args, dfs, func, kwargs, meta, parent_meta)
       6750         # Use non-normalized kwargs here, as we want the real values (not
       6751         # delayed values)
    -> 6752         meta = _emulate(func, *args, udf=True, **kwargs)
       6753         meta_is_emulated = True
       6754     else:
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in _emulate(func, udf, *args, **kwargs)
       6569     """
       6570     with raise_on_meta_error(funcname(func), udf=udf), check_numeric_only_deprecation():
    -> 6571         return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))
       6572 
       6573 
    /usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/contextlib.py in __exit__(self, type, value, traceback)
        129                 value = type()
        130             try:
    --> 131                 self.gen.throw(type, value, traceback)
        132             except StopIteration as exc:
        133                 # Suppress StopIteration *unless* it's the same exception that
    /usr/local/lib/python3.8/site-packages/dask/dataframe/utils.py in raise_on_meta_error(funcname, udf)
        214         )
        215         msg = msg.format(f" in `{funcname}`" if funcname else "", repr(e), tb)
    --> 216         raise ValueError(msg) from e
        217 
        218 
    ValueError: Metadata inference failed in `query`.
    
    You have supplied a custom function and Dask is unable to 
    determine the type of output that that function returns. 
    
    To resolve this please provide a meta= keyword.
    The docstring of the Dask function you ran should have more information.
    
    Original error is below:
    ------------------------
    UndefinedVariableError("name 'Bob' is not defined")
    
    Traceback:
    ---------
      File "/usr/local/lib/python3.8/site-packages/dask/dataframe/utils.py", line 195, in raise_on_meta_error
        yield
      File "/usr/local/lib/python3.8/site-packages/dask/dataframe/core.py", line 6571, in _emulate
        return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))
      File "/usr/local/lib/python3.8/site-packages/dask/utils.py", line 1103, in __call__
        return getattr(__obj, self.method)(*args, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/frame.py", line 3340, in query
        res = self.eval(expr, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/frame.py", line 3470, in eval
        return _eval(expr, inplace=inplace, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/eval.py", line 341, in eval
        parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 787, in __init__
        self.terms = self.parse()
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 806, in parse
        return self._visitor.visit(self.expr)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 398, in visit
        return visitor(node, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 404, in visit_Module
        return self.visit(expr, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 398, in visit
        return visitor(node, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 407, in visit_Expr
        return self.visit(node.value, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 398, in visit
        return visitor(node, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 699, in visit_Compare
        return self.visit(binop)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 398, in visit
        return visitor(node, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 520, in visit_BinOp
        op, op_class, left, right = self._maybe_transform_eq_ne(node)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 441, in _maybe_transform_eq_ne
        right = self.visit(node.right, side="right")
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 398, in visit
        return visitor(node, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 533, in visit_Name
        return self.term_type(node.id, self.env, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/ops.py", line 84, in __init__
        self._value = self._resolve_name()
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/ops.py", line 101, in _resolve_name
        res = self.env.resolve(self.local_name, is_local=self.is_local)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/scope.py", line 204, in resolve
        raise UndefinedVariableError(key, is_local) from err

またpandasでも似たようなエラーが出る。

# pandas 変数名を使用 f文字列 失敗例
target = 'Bob'
df.query(f"name=={target}")
# --------------------
エラー。長いので折りたたみます。

クリックでエラー内容を表示

    KeyError                                  Traceback (most recent call last)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/scope.py in resolve(self, key, is_local)
        187             if self.has_resolvers:
    --> 188                 return self.resolvers[key]
        189 
    /usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/collections/__init__.py in __getitem__(self, key)
        897                 pass
    --> 898         return self.__missing__(key)            # support subclasses that define __missing__
        899 
    /usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/collections/__init__.py in __missing__(self, key)
        889     def __missing__(self, key):
    --> 890         raise KeyError(key)
        891 
    KeyError: 'Bob'
    
    During handling of the above exception, another exception occurred:
    KeyError                                  Traceback (most recent call last)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/scope.py in resolve(self, key, is_local)
        198                 # e.g., df[df > 0]
    --> 199                 return self.temps[key]
        200             except KeyError as err:
    KeyError: 'Bob'
    
    The above exception was the direct cause of the following exception:
    UndefinedVariableError                    Traceback (most recent call last)
    <ipython-input-18-52c23030a7f6> in <module>
          1 # pandas 変数名を使用 f文字列 失敗例
          2 target = 'Bob'
    ----> 3 df.query(f"name=={target}")
    
    /usr/local/lib/python3.8/site-packages/pandas/core/frame.py in query(self, expr, inplace, **kwargs)
       3338         kwargs["level"] = kwargs.pop("level", 0) + 1
       3339         kwargs["target"] = None
    -> 3340         res = self.eval(expr, **kwargs)
       3341 
       3342         try:
    /usr/local/lib/python3.8/site-packages/pandas/core/frame.py in eval(self, expr, inplace, **kwargs)
       3468         kwargs["resolvers"] = kwargs.get("resolvers", ()) + tuple(resolvers)
       3469 
    -> 3470         return _eval(expr, inplace=inplace, **kwargs)
       3471 
       3472     def select_dtypes(self, include=None, exclude=None) -> "DataFrame":
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/eval.py in eval(expr, parser, engine, truediv, local_dict, global_dict, resolvers, level, target, inplace)
        339         )
        340 
    --> 341         parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
        342 
        343         # construct the engine and evaluate the parsed expression
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in __init__(self, expr, engine, parser, env, level)
        785         self.parser = parser
        786         self._visitor = _parsers[parser](self.env, self.engine, self.parser)
    --> 787         self.terms = self.parse()
        788 
        789     @property
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in parse(self)
        804         Parse an expression.
        805         """
    --> 806         return self._visitor.visit(self.expr)
        807 
        808     @property
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        396         method = "visit_" + type(node).__name__
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
        400     def visit_Module(self, node, **kwargs):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Module(self, node, **kwargs)
        402             raise SyntaxError("only a single expression is allowed")
        403         expr = node.body[0]
    --> 404         return self.visit(expr, **kwargs)
        405 
        406     def visit_Expr(self, node, **kwargs):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        396         method = "visit_" + type(node).__name__
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
        400     def visit_Module(self, node, **kwargs):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Expr(self, node, **kwargs)
        405 
        406     def visit_Expr(self, node, **kwargs):
    --> 407         return self.visit(node.value, **kwargs)
        408 
        409     def _rewrite_membership_op(self, node, left, right):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        396         method = "visit_" + type(node).__name__
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
        400     def visit_Module(self, node, **kwargs):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Compare(self, node, **kwargs)
        697             op = self.translate_In(ops[0])
        698             binop = ast.BinOp(op=op, left=node.left, right=comps[0])
    --> 699             return self.visit(binop)
        700 
        701         # recursive case: we have a chained comparison, a CMP b CMP c, etc.
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        396         method = "visit_" + type(node).__name__
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
        400     def visit_Module(self, node, **kwargs):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_BinOp(self, node, **kwargs)
        518 
        519     def visit_BinOp(self, node, **kwargs):
    --> 520         op, op_class, left, right = self._maybe_transform_eq_ne(node)
        521         left, right = self._maybe_downcast_constants(left, right)
        522         return self._maybe_evaluate_binop(op, op_class, left, right)
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in _maybe_transform_eq_ne(self, node, left, right)
        439             left = self.visit(node.left, side="left")
        440         if right is None:
    --> 441             right = self.visit(node.right, side="right")
        442         op, op_class, left, right = self._rewrite_membership_op(node, left, right)
        443         return op, op_class, left, right
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        396         method = "visit_" + type(node).__name__
        397         visitor = getattr(self, method)
    --> 398         return visitor(node, **kwargs)
        399 
        400     def visit_Module(self, node, **kwargs):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit_Name(self, node, **kwargs)
        531 
        532     def visit_Name(self, node, **kwargs):
    --> 533         return self.term_type(node.id, self.env, **kwargs)
        534 
        535     def visit_NameConstant(self, node, **kwargs):
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/ops.py in __init__(self, name, env, side, encoding)
         82         tname = str(name)
         83         self.is_local = tname.startswith(_LOCAL_TAG) or tname in _DEFAULT_GLOBALS
    ---> 84         self._value = self._resolve_name()
         85         self.encoding = encoding
         86 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/ops.py in _resolve_name(self)
         99 
        100     def _resolve_name(self):
    --> 101         res = self.env.resolve(self.local_name, is_local=self.is_local)
        102         self.update(res)
        103 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/scope.py in resolve(self, key, is_local)
        202                 from pandas.core.computation.ops import UndefinedVariableError
        203 
    --> 204                 raise UndefinedVariableError(key, is_local) from err
        205 
        206     def swapkey(self, old_key: str, new_key: str, new_value=None):
    UndefinedVariableError: name 'Bob' is not defined

これはquery関数の中にある文字列を単独で表示させるとよく分かる。

print(f"name=={target}")

# --------------------

name==Bob

Bobに引用符をつけなければいけないのに、ついていない。これではBobはただの列名として扱われるはずだ。
正しい結果を得るためには、f文字列の中、Bobの外側に引用符を書く必要がある。

# dask 変数名を使用 f文字列 成功例
target = 'Bob'
ddf.query(f"name=='{target}'").compute()

# --------------------

  name item  number id_code
1  Bob  bbb       2     123
5  Bob  fff       1     345
# pandas 変数名を使用 実はf文字列でも行ける
target = 'Bob'
df.query(f"name=='{target}'")

# --------------------

  name item  number id_code
1  Bob  bbb       2     123
5  Bob  fff       1     345

数字が入っている文字列型の場合

データのうち、id_codeカラムが"123"であるものを抽出しよう。

# pandas 直接値を指定
df.query("id_code=='123'")

# --------------------

      name item  number id_code
1      Bob  bbb       2     123
3  Charlie  ddd       3     123
# dask 直接値を指定
ddf.query("id_code=='123'").compute()

# --------------------

      name item  number id_code
1      Bob  bbb       2     123
3  Charlie  ddd       3     123

ここまでは何も問題ない。
ところが、変数名を使用すると状況が変わってくる。

# pandas 変数名を使用 @
code = '123'
df.query(f"id_code==@code")

# --------------------

      name item  number id_code
1      Bob  bbb       2     123
3  Charlie  ddd       3     123
# dask 変数名を使用 f文字列 失敗例
code = '123'
ddf.query(f"id_code=={code}").compute()

# --------------------

Empty DataFrame
Columns: [name, item, number, id_code]
Index: []

query関数の結果は空のDataFrameになる。
エラーが出るほうがまだハッキリ間違い箇所が分かる分だけ修正しやすいかもしれない……
これもquery関数の中にある文字列を単独で表示させるとよく分かる。

print(f"id_code=={code}")

# --------------------

id_code==123

これをquery関数に入れると、id_codeが数字の123に等しいものを探してしまう。だから該当する行は無く、空のDataFrameが返る。
なお、pandasでは数字(より正確には10進数の整数リテラル)の先頭に0をつけてはいけないので、012で同じことをやると違う状況になる。

# pandasでは数字の先頭に0をつけてはいけない
x = 012

# --------------------

  File "<ipython-input-27-d581e4a9bb8c>", line 2
    x = 012
          ^
SyntaxError: leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers
# dask 変数名を使用 f文字列 失敗例その2
code = '012'
ddf.query(f"id_code=={code}").compute()
# --------------------
エラー。長いので折りたたみます。

クリックでエラー内容を表示


    SyntaxError                               Traceback (most recent call last)
    /usr/local/lib/python3.8/site-packages/dask/dataframe/utils.py in raise_on_meta_error(funcname, udf)
        194     try:
    --> 195         yield
        196     except Exception as e:
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in _emulate(func, udf, *args, **kwargs)
       6570     with raise_on_meta_error(funcname(func), udf=udf), check_numeric_only_deprecation():
    -> 6571         return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))
       6572 
    /usr/local/lib/python3.8/site-packages/dask/utils.py in __call__(self, _methodcaller__obj, *args, **kwargs)
       1102     def __call__(self, __obj, *args, **kwargs):
    -> 1103         return getattr(__obj, self.method)(*args, **kwargs)
       1104 
    /usr/local/lib/python3.8/site-packages/pandas/core/frame.py in query(self, expr, inplace, **kwargs)
       3339         kwargs["target"] = None
    -> 3340         res = self.eval(expr, **kwargs)
       3341 
    /usr/local/lib/python3.8/site-packages/pandas/core/frame.py in eval(self, expr, inplace, **kwargs)
       3469 
    -> 3470         return _eval(expr, inplace=inplace, **kwargs)
       3471 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/eval.py in eval(expr, parser, engine, truediv, local_dict, global_dict, resolvers, level, target, inplace)
        340 
    --> 341         parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
        342 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in __init__(self, expr, engine, parser, env, level)
        786         self._visitor = _parsers[parser](self.env, self.engine, self.parser)
    --> 787         self.terms = self.parse()
        788 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in parse(self)
        805         """
    --> 806         return self._visitor.visit(self.expr)
        807 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        393                     e.msg = "Python keyword not valid identifier in numexpr query"
    --> 394                 raise e
        395 
    /usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py in visit(self, node, **kwargs)
        389             try:
    --> 390                 node = ast.fix_missing_locations(ast.parse(clean))
        391             except SyntaxError as e:
    /usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ast.py in parse(source, filename, mode, type_comments, feature_version)
         46     # Else it should be an int giving the minor version for 3.x.
    ---> 47     return compile(source, filename, mode, flags,
         48                    _feature_version=feature_version)
    SyntaxError: invalid syntax (<unknown>, line 1)
    
    The above exception was the direct cause of the following exception:
    ValueError                                Traceback (most recent call last)
    <ipython-input-28-5004d24aebb2> in <module>
          1 # dask 変数名を使用 f文字列 失敗例その2
          2 code = '012'
    ----> 3 ddf.query(f"id_code=={code}").compute()
    
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in query(self, expr, **kwargs)
       5178         2  1  3    2
       5179         """
    -> 5180         return self.map_partitions(M.query, expr, **kwargs)
       5181 
       5182     @derived_from(pd.DataFrame)
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in map_partitions(self, func, *args, **kwargs)
        873         None as the division.
        874         """
    --> 875         return map_partitions(func, self, *args, **kwargs)
        876 
        877     @insert_meta_param_description(pad=12)
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in map_partitions(func, meta, enforce_metadata, transform_divisions, align_dataframes, *args, **kwargs)
       6639     dfs = [df for df in args if isinstance(df, _Frame)]
       6640 
    -> 6641     meta = _get_meta_map_partitions(args, dfs, func, kwargs, meta, parent_meta)
       6642     if all(isinstance(arg, Scalar) for arg in args):
       6643         layer = {
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in _get_meta_map_partitions(args, dfs, func, kwargs, meta, parent_meta)
       6750         # Use non-normalized kwargs here, as we want the real values (not
       6751         # delayed values)
    -> 6752         meta = _emulate(func, *args, udf=True, **kwargs)
       6753         meta_is_emulated = True
       6754     else:
    /usr/local/lib/python3.8/site-packages/dask/dataframe/core.py in _emulate(func, udf, *args, **kwargs)
       6569     """
       6570     with raise_on_meta_error(funcname(func), udf=udf), check_numeric_only_deprecation():
    -> 6571         return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))
       6572 
       6573 
    /usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/contextlib.py in __exit__(self, type, value, traceback)
        129                 value = type()
        130             try:
    --> 131                 self.gen.throw(type, value, traceback)
        132             except StopIteration as exc:
        133                 # Suppress StopIteration *unless* it's the same exception that
    /usr/local/lib/python3.8/site-packages/dask/dataframe/utils.py in raise_on_meta_error(funcname, udf)
        214         )
        215         msg = msg.format(f" in `{funcname}`" if funcname else "", repr(e), tb)
    --> 216         raise ValueError(msg) from e
        217 
        218 
    ValueError: Metadata inference failed in `query`.
    
    You have supplied a custom function and Dask is unable to 
    determine the type of output that that function returns. 
    
    To resolve this please provide a meta= keyword.
    The docstring of the Dask function you ran should have more information.
    
    Original error is below:
    ------------------------
    SyntaxError('invalid syntax', ('<unknown>', 1, 13, 'id_code ==0 12 \n'))
    
    Traceback:
    ---------
      File "/usr/local/lib/python3.8/site-packages/dask/dataframe/utils.py", line 195, in raise_on_meta_error
        yield
      File "/usr/local/lib/python3.8/site-packages/dask/dataframe/core.py", line 6571, in _emulate
        return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))
      File "/usr/local/lib/python3.8/site-packages/dask/utils.py", line 1103, in __call__
        return getattr(__obj, self.method)(*args, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/frame.py", line 3340, in query
        res = self.eval(expr, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/frame.py", line 3470, in eval
        return _eval(expr, inplace=inplace, **kwargs)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/eval.py", line 341, in eval
        parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 787, in __init__
        self.terms = self.parse()
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 806, in parse
        return self._visitor.visit(self.expr)
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 394, in visit
        raise e
      File "/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py", line 390, in visit
        node = ast.fix_missing_locations(ast.parse(clean))
      File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ast.py", line 47, in parse
        return compile(source, filename, mode, flags,

正しい結果を得るためには、f文字列の中、123の外側に引用符を書く必要がある。

# dask 変数名を使用 f文字列 成功例
code = '123'
ddf.query(f"id_code=='{code}'").compute()
# '012' の場合も同様なので、省略する。

# --------------------

      name item  number id_code
1      Bob  bbb       2     123
3  Charlie  ddd       3     123

まとめ

「pandas側で@variable_nameと書く代わりに、daskでは{variable_name}と書く」という意識だと失敗する。
「変数を使わずにquery関数の引数の文字列を書くにはどうすればよいか」「それをf文字列で実現するにはどうすればよいか」 を考えれば良さそう。長々と色々な例を書いてきたけど、要約すれば上記のとおりになる。
daskのqueryの場合、扱っているのは普通のf文字列なので、文字列内の変数を展開したときに期待通りになっていれば良いというわけだ。

……というかここまで書いて気づいたけど、 pandasのqueryも引数に取るのはただの文字列なんだから、

num = 2
df.query(f"number=={num}")

が行けるとか書いてたけど、ただの文字列の書き方の違いじゃん。引数の文字列をそのまま書くかf文字列の展開を使って書くかの違いじゃん。
pandasのqueryといえば@を使うのが当たり前で、f文字列でも上手くいくのが意外で特別なことのように見えてしまった。 しかし、むしろ@を使った記法の方が、引数文字列の中身が違うから特殊だった(pandasの特殊な記法)。f文字列による指定は普通のpythonが分かっていれば自然な、一般的な話であった。

それでは。

docker image pruneで「何日前より前のイメージを全て削除」を指定する

Dockerイメージを作ってAmazon ECSにプッシュ、を繰り返していた。その結果、docker imagesコマンドを打つと、使っていない(最新でない)dockerイメージが大量に表示されてわかりにくくなってしまった。イメージを削除する方法を調べた。

Q1. Docker image を多数一斉に消すためのコマンドは何か?

A1. docker image prune

ドキュメントはこちら。
https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/commandline/image_prune/
…… docker.com のドメインの下に無いんだけど、これって公式なのか? 個人的な翻訳なのか? 分からん。
https://docs.docker.jp/engine/reference/commandline/image_prune.html
ドメインにdockerが入っているのはこっちですね。

docker image rm もイメージを削除するコマンドだが、イメージのidを指定する必要があるので、一定条件に当てはまるイメージをまとめて削除はできない。

Q2. デフォルトだとどの範囲が消えるの?

A2. 宙ぶらりんなイメージ = 「タグを持たず、他のコンテナからも参照されないイメージ」

全ての宙ぶらりんな(dangling)イメージを削除します。(先ほどのドキュメントより)
って言われても分からないな。別のページの説明によれば「タグを持たず、他のコンテナからも参照されないイメージ」のこと。

宙ぶらりんイメージとは、タグを持たず、他のコンテナからも参照されないイメージです。
https://docs.docker.jp/config/pruning.html

Q3 untilの使い方は?

A3. ある時点より手前の宙ぶらりんなイメージを全部削除する。

以下2つの方式が可能。

  • 具体的なタイムスタンプを指定して「until=2017-01-04T00:00:00」とするか、
  • 現在からの相対時間で「until=240h」とするか

Q4 3日前よりも前に作られた宙ぶらりんな(dangling)imageを全部消したい。どのようにコマンドを打てばよいか?

A4. until=3d も until=3D も不可。until=72hとする必要あり。

3日は3dで行けるのかなーとやってみたら、

> docker image prune --filter until=3d
WARNING! (省略。yを入力して実行する)
Error response from daemon: failed to parse as time or duration: "3d"

とエラーになったので、「3日を指定する方法ってどうするんだろう」と思ってドキュメントを見ると、こう書いてあった。

デーモンが動作しているマシン上の時刻からの相対時間を、 Go duration 文字列(例: 10m 、 1h3-m )で計算します。 https://docs.docker.jp/engine/reference/commandline/image_prune.html

じゃあこの書式を調べようと思って「Go duration 文字列」で検索しても、情報が出てこない。「Go duration」で調べると出てくる。
https://leben.mobi/go/time/go-programming/#timeDuration
Go言語でtimeパッケージのtime.Duration型は、2つの時刻の差を表す型である。pythonだとdatetime.timedeltaに相当するものだな。
そしてその単位は最大で"h(時間)"までしか対応していない。

Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
https://pkg.go.dev/time#Duration

というわけで3日をそのまま指定することはできない。72時間に単位換算して、

> docker image prune --filter until=72h

とするのが正解だ。

慣れないDockerを使い始めた初心者の覚え書きでした。それでは。