mizzsugar’s blog

日々感じていることや学んだことを書きます。エンジニアリング以外にも書くかもしれません。

PyCon mini Shizuokaでオンライン登壇しました。

先日、PyCon mini Shizuokaで「unittest.mockを使ってテストを書こう 〜モックオブジェクトを使ってより単体テストの目的に沿ったテストに〜」という題で30分の発表をさせていただきました。

shizuoka.pycon.jp


この発表をすると決めた経緯

もともと、去年の10月の「みんなのPython勉強会」で登壇オファーをいただいたのが始まりでした。「せっかくのチャンスだから登壇しよう。(当時は自分の中でテスト熱が高かったので)テストについて話したい。けど、それだと広すぎるのであまり発表を聞かないモックについて話そう」というふうに決めました。

いざ、登壇してみると、参加者はプログラミング自体の初学者が多く、「よくわからなかった」という声をちょくちょく耳にしました。

想定していた層は「テストコードを書いたことがあるけれども自分のテストコードに自信がない人」だったので、当然いえば当然かもしれませんが、誰も悪くないとはいえショックでした。


振り返りのブログでは、

もしこのトークでもう1回登壇するならば
とはいえ「モック」の概念の説明が不足していたのでモックの概念をもう少ししっかり説明する

本題に入る前に予めこの発表の対象となる人を伝えておく

「モック」と「スタブ」の概念も扱って、どんな観点で単体テストを書くかをより深く扱う

対話を通してでないと理解が難しいであろう質問は2-3分の質疑応答の時間で理解されなくても落ち込まない。交流会とかで詳しく話そうねというスタンスで挑む。

Chromeで登壇する

で行こうと思います。

と書いていますが

これをやるには少なくとも30分は必要そうです。

mizzsugar.hatenablog.com

30分以上話せるイベントはなかなかなありません。リベンジはそう早くはないかなあと思っていたところ、PyCon mini Shizukokaの存在を知りました。

言葉に厳しい人と惹きつける文章を書くのが得意な人の協力の元CfPを書いた結果、CfP採択していただきました。

CfPの項目のうち、HPに載るトーク概要は最終的にこのようになりました。

shizuoka.pycon.jp

発表の練習は主に、今年の1月のPyhack冬合宿で行いました。

2泊3日の合宿で計3回話しました。

こんな感じでPython猛者の方々にSlackでフィードバック書いてもらうなどしました。

発表の仕方だけでなく、Pythonについて新しい知見を得てスライドをパワーアップさせることができました。


具体的なところでいうと、datetime.datetime型の関数型オブジェクトを引数に渡す方法やresponsesをテストで使うことはこの合宿で教えてもらいました。

datetime.datetime型の関数型オブジェクトを引数に渡す方法のスライド↓

https://speakerdeck.com/mizzsugar/unittest-dot-mockwoshi-tutetesutowoshu-kou-motukuobuziekutowoshi-tuteyoridan-ti-tesutofalsemu-de-niyan-tutatesutoni?slide=47

responsesをテストで使うスライド↓

https://speakerdeck.com/mizzsugar/unittest-dot-mockwoshi-tutetesutowoshu-kou-motukuobuziekutowoshi-tuteyoridan-ti-tesutofalsemu-de-niyan-tutatesutoni?slide=52

オンラインで登壇してみた所感

結論から言うと、オンラインで話すのに慣れていなくて心残りがあるのですが

短い期間で急遽オンラインカンファレンスに切り替え準備をしてくださった運営の方々には感謝してもしきれないです。ありがとうございました。

対面で誰か聞いてくれている人を見つけて安心しようとする方面で練習していたので、オンラインで一人で喋るのは緊張しました。

YoutubeTwitterで発表を聞いてる人のコメントを見れましたが、なかったら動揺しそうだったので見ませんでした笑

接続に時間がかかってしまったため、時間内に収めるために前半がちょっと早足になってしまったのが心残りです。

でも、発表が終わった後に恐る恐るTwitterYoutubeの反応を見るとコメントがあって安心しました。

特に、にしもつさんとnikkieさんがほぼ全てのトークに関してTwitterで随時呟いてくださっていてすごく安心しました。ありがとうございます。

ちなみに、前回の反省点に関しては下記のような結果になりました。

とはいえ「モック」の概念の説明が不足していたのでモックの概念をもう少ししっかり説明する →達成

本題に入る前に予めこの発表の対象となる人を伝えておく →達成

「モック」と「スタブ」の概念も扱って、どんな観点で単体テストを書くかをより深く扱う →「モック」と「スタブ」の概念についてはこのトークだけでは扱いきれないと判断し断念。単体テストの観点に関しては達成。

対話を通してでないと理解が難しいであろう質問は2-3分の質疑応答の時間で理解されなくても落ち込まない。交流会とかで詳しく話そうねというスタンスで挑む。→達成

Chromeで登壇する →達成

次オンラインカンファレンスで登壇するとしたら、一人で喋っても寂しく思わない練習をして挑みたいです。

また、次のPyCon mini Shizuokaが開催されるならば、登壇した次の日にさわやかハンバーグ食べて美味しいコーヒー飲みに行きたいです。

PythonとTypeScriptで学ぶGenerics初めの一歩

TwitterGenericsの話が浮上しているのに影響されて、Generics使うと何が良いのか落とし込みました。

なお、この2つの言語にしたのは、普段自分が使うからです。

Genericsとは

「総称型」とか「汎用型」と言われます。 型定義にGenericsを使うことで文字列型や数値型など具体的な型に依存しない 抽象的かつ汎用的な関数やクラスを作ることができます。

そもそも、「generic」という単語は「汎用の」という意味があります。 そこから、「Generics」とは汎用的な何かを指しているんだなと想像できます。

Pythonでの定義の仕方

sample.py

from typing import TypeVar, Sequence

T = TypeVar(’T’)  # TypeVarを使ってTという名前の型だと宣言します。

def fist(l: Sequence[T]) -> T:  # 渡された配列の一番最初の要素を返す関数
    return l[0]

TypeScriptでの定義の仕方

sample.ts

function first<T> (l: T[]): T {
  return l[0]
}

サンプルコードはPythonのドキュメントから。

typing --- 型ヒントのサポート — Python 3.8.2 ドキュメント

Genericsを使わないとどうなる?

最初の要素を返す関数をGenericsを使わないで書いてみましょう。

実装し始めた頃は要素が文字列の場合のみ想定されていたとします。

Python

sample.py

from typing import Sequence


def fist(l: Sequence[str]) -> str:
    return l[0]

Typescript

sample.ts

function first(l: string[]): string {
  return l[0]
}

後々、数値やオブジェクトにも使いたいという要望が出ます。

Python

from typing import Sequence

from item import Item   // 自作クラス


def fist_str(l: Sequence[str]) -> str:
    return l[0]

def fist_int(l: Sequence[int]) -> int:
    return l[0]

def fist_item(l: Sequence[Item]) -> Item:
    return l[0]

TypeScript

import { Item } from "@/src/item"  // 自作クラス


function first_string(l: string[]): string {
  return l[0]
}

function first_number(l: number[]): number {
  return l[0]
}

function first_item(l: Item[]): Item {
  return l[0]
}

これだと、型だけ異なる同じ内容の処理の関数が型の数だけ書かないといけなくなります。

色んな型で使いたい、けどAnyではなくその型じゃないといけないよと宣言したい処理を書く時にGenericsは便利だと思いました。

例えばこれだと、lが数値の配列である時に返り値が文字列でも数値でも真偽値でもなんでもOKになってしまいます。

Python

from typing import Any, Sequence


def fist_item(l: Sequence[Any]) -> Any:
    return l[0]

TypeScript

function first (l: any[]): any {
  return l[0]
}

Genericsがないと汎用的なライブラリやフレームワークの実装で型の数だけ関数やクラスを書いているとだいぶ辛いそうだなと想像しました。

辛くならないようにGenericsを使いこなせるようになりたいなと思います。

【TypeScript】型定義のためだけにimportしたクラスやインターフェースがno-unused-vars扱いされて困った話

原因を知れば大したことなかったんですけど解決まで苦戦したので備忘録を残します。


linterはeslintを使っています。

Typescript 3.7
Typescript-eslint-plugin 2.3


pages/user/registration

  
<template>
  <div class="container">
    <user-edit
      :on-submit-callback="onSubmit"
    />
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { AxiosResponse } from 'axios'
import UserEdit from '@/components/UserEdit.vue'  // フォームのコンポーネントです。
import { DraftUser, User } from '@/libs/sampleapi'

@Component({
  components: {
    UserEdit
  }
})
class Page extends Vue {
  async onSubmit (draft: DraftUser) {
    const draftUser: DraftUser = {
      name: draft.name,
      email: draft.email,
      password: draft.password
    }
    const [success, response] = await (async (): Promise<[boolean, AxiosResponse<User>]> => {  // AxiosResponseとUserがno-unused-varsとされます。
      try {
        const response = await this.$sampleapi.registerUser({ draft: DraftUser })  // ユーザーを登録するAPIです。DraftUserがno-unused-varsとされます。
        return [true, response]
      } catch (err) {
        return [false, err.response]
      }
    })()
    if (success) {
      this.$toast.success('ユーザーを登録しました。')  // 成功した旨のトーストを表示します。
      this.$router.push(`/user/${response.data.id}`)  // 登録したユーザーの詳細画面に遷移します。
    } else {
      this.$toast.error('ユーザーを登録に失敗しました。')  // 失敗した旨のトーストを表示します。
      // エラーの描画は省略します。
    }
  }
}
export default Page
</script>


APIはこんな感じです。Axiosでの通信部分はOpenAPI generatorのコマンドで作ってくれるのですが

作成してくれたaxiosの部分のソースが人間に優しくなかったのでyamlファイルだけ掲載します。

openapi.yaml

openapi: 3.0.2
info:
  title: SAMPLE API
  version: 0.0.0
paths:
  /auth.register_user:
    post:
      summary: ユーザーを登録します。
      operationId: register_user
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                host_id:
                  type: integer
                draft_plan:
                  $ref: '#/components/schemas/DraftUser'
              required:
                - draft
      responses:
        201:
          description: ユーザーを登録しました。
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        400:
          description: リクエストのパラメータが不当です。
          content:
            application/json:
              schema:
                type: object
                properties:
                  errors:
                    type: array
                    items:
                      $ref: '#/components/schemas/ParameterError'
                required:
                  - errors
components:
  schemas:
    DraftUser:
      type: object
      description: 登録対象のユーザー情報
      properties:
        name:
          type: string
        email:
          type: string
        password:
          type: string
      required:
        - name
        - email
        - password
    User:
      type: object
      description: 登録済みのユーザー
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
      required:
        - id
        - name
        - email
    ParameterError:
      type: object
      properties:
        field:
          type: string
        code:
          type: string
      required:
        - field
        - code
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

Linterコマンドを実行すると

eslint --ext .js,.vue,.ts, --ignore-pattern **/*.d.ts --ignore-path .gitignore

AxiosResponseとUserとDraftUserがno-unused-varsでひっかかってしまいました…


eslintrcの内容が原因でした。

eslintrc.js

extends: [

 'plugin:nuxt/recommended’,
 '@nuxtjs',
],
plugins: [
 'typescript’,
 '@typescript-eslint’,
],
rules {
 'typescript/no-unused-vars': 'error',
…
}

こちらです。

'typescript/no-unused-vars': 'error',

typescript-eslintを使っているので@typescript-eslintのno-unused-varsにすべきでした。

~~’typescript/no-unused-vars': 'error',
"@typescript-eslint/no-unused-vars": ["error", { args: "none"}],

Improve documentation for `typescript/no-unused-vars` · Issue #46 · typescript-eslint/typescript-eslint · GitHub

また、argsは引数に使われているかどうかのチェックに使われます。 関数の引数に使われていなくても良いのでnoneとしました。

Noneの他のオプションだと Aster-usedでは、最後に利用された後に利用される位置引数全てがチェックされます。最後に利用された引数の前の位置引数はチェックされません。

/*eslint no-unused-vars: ["error", { "args": "after-used" }]*/

// 2 errors, for the parameters after the last used parameter (bar)
// "baz" is defined but never used
// "qux" is defined but never used
(function(foo, bar, baz, qux) {
  return bar;
})();

Allでは定義された引数は全て使われないといけません。

https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md#args

そのif文、Enumにしてみませんか。

これは、Python Advent Calendar 17日目の記事です。

Python Advent Calendar 2019 - Qiita

この記事では、私が好きな標準モジュールのうちの一つenumモジュールの基本的な使い方と enumモジュールを使ってリファクタリングする例を紹介します。

enumモジュールとは

列挙型クラスをサポートします。

列挙型とは

Pythonのドキュメントにはこう書かれています。

列挙型は、一意の定数値に束縛された識別名 (メンバー) の集合です。列挙型の中でメンバーの同一性を比較でき、列挙型自身でイテレートが可能です。

https://docs.python.org/ja/3.8/library/enum.html

enum.Enumを使用した例

例えば、血液型を表すクラスがあるとします。 血液型はA, B, O, ABの4種類から成り立っています。 (より細かい分類がありますが、ここではこの4種類のみ使うとします)

一般的なクラスで表現するとこうなります。

class BloodType:
    Def __init__(self, type: str):
        self. type = type


ただ、上記の例だとA, B, O, AB以外の文字列が入ることを許可します。

ということで、__init__内で検知します。

class BloodType:
    Def __init__(self, type: str):
        if type not in (‘A’, ‘B’, ‘O’, ‘AB’):
            raise ValueError(‘妥当な血液型ではありません’)
        self. type = type


これも悪くないですが、if文を使わなくても 正しい値をとる方法があります。

血液型を列挙型クラスで表現することです。

import enum


class BloodType(enum.Enum):
    A = 'A'
    B = 'B'
    O = 'O'
    AB = 'AB'

列挙型クラスの方が、if文で書かれるよりも 血液型はどんな値が入っているのか 明示的に表現できて個人的には好きです。

血液型やトランプの記号(ハートとかダイヤとか)など

どの値を使うのか決まっているものを列挙型で表現するという選択肢を知っていると

安全でわかりやすいクラスを書くのに役に立ちます。


enumモジュールの基本的な使い方

import enum


class BloodType(enum.Enum):
       A = 'A型'
       B = 'B型'
       O = 'O型'
       AB = 'AB型'
>>> import app.example
>>>
>>>
>>> app.example.BloodType.O
<BloodType.O: 'O型'>
>>> app.example.BloodType.O.value
'O型'
>>> app.example.BloodType.O.name
'O'


ダンダーメソッドや一般的なメソッドもクラスに作れる

例えばこんな風に。

class BloodType(enum.Enum):
    A = 'A型'
    B = 'B型'
    O = 'O型'
    AB = 'AB型'

    def __str__(self):
        return self.name

    @classmethod
    def show_all(cls) -> List[str]:
        return list(map(lambda c: c.value, cls))


enum.auto, enum.unique, enum.IntEnum, enum.Enumの使いどころ

enum.auto

先程の血液型の例ですが、AかBかOかABかそれぞれ識別できれば良いだけで値は何でも良いとします。

そのような時に、enum.autoを使うと値をPythonが決定してくれます。

import enum


class BloodType(enum.Enum):
    A = enum.auto()
    B = enum.auto()
    O = enum.auto()
    AB = enum.auto()

自動で、1, 2, 3, 4...と振ってくれます。


enum.unique

通常のEnumだと、列挙型メンバーに同じ名前をつけることは出来ませんが、同じ値に設定することはできます。

>>> import enum
>>>
>>>
>>> class BloodType(enum.Enum):
...     A = 'A'
...     A = 'B'
...     
... 
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    class BloodType(enum.Enum):
  File "<input>", line 3, in BloodType
    A = 'B'
  File "/home/mizzsugar/.pyenv/versions/3.8.0/lib/python3.8/enum.py", line 95, in __setitem__
    raise TypeError('Attempted to reuse key: %r' % key)
TypeError: Attempted to reuse key: 'A'


>>> import enum
>>>
>>>
>>> class BloodType(enum.Enum):
...     A = 'A'
...     B = 'A'

enum.uniqueは、valueが重複してほしくない時に使います。

>>> import enum
>>>
>>>
>>> @enum.unique
... class BloodType(enum.Enum):
...     A = 'A'
...     B = 'A'
...     
... 
Traceback (most recent call last):
  File "<input>", line 2, in <module>
    class BloodType(enum.Enum):
  File "/home/mizzsugar/.pyenv/versions/3.8.0/lib/python3.8/enum.py", line 860, in unique
    raise ValueError('duplicate values found in %r: %s' %
ValueError: duplicate values found in <enum 'BloodType'>: B -> A


enum.IntEnum

enum.Enumではvalueがintやfloatなどの数値であっても、int型やfloat型のオブジェクトと比較ができません。

>>> import enum
>>> class BloodType(enum.Enum):
...     A = 1
...     B = 2
...     O = 3
...     AB = 4
...
...
>>> BloodType.A == 1
False

しかし、IntEnumだと比較することができます。

>>> import enum
>>> class BloodType(enum.IntEnum):
...     A = 1
...     B = 2
...     O = 3
...     AB = 4
...
...
>>> BloodType.A == 1
True

例えば、他の場所から取得した数値とIntEnumの値を比較または計算したい時に役に立ちます。(ex: データベースから数値を取得して比較したい時など)


enumモジュールを使ったリファクタリング

「現場で役立つシステム設計の原則」 の第2章で書かれている申請管理システムをPythonで書き直した例を使って締めたいと思います。

申請管理システムの、申請状況を管理する部分を書きます。

ざっくり説明すると、最初になにかしたらの申請があって審査が始まったらその申請のステータスは「審査中」になる、みたいなやつです。

ここでのポイントは、ステータスが「承認済み」の申請が「差し戻し中」に変わらないように、といった意図しないステータスに遷移しないようにもしないといけないところです。

申請管理を適切な英語に直す時間がなかったので日本語プログラミングです。

https://www.amazon.co.jp/dp/B073GSDBGT/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1


まず、Enum書かないで自分が考えた中で一番読みにくい例で。

def 遷移可能かチェックする(_from: str, to: str) -> bool:
    if _from == '審査中' and to in {'差し戻し中', '承認済み'}:
        return True
    if _from == '差し戻し中' and to in {'審査中', '終了'}:
        return True
    if _from == '承認済み' and to in {'実施中', '終了'}:
        return True
    if _from == '実施中' and to in {'終了', '中断中'}:
        return True
    if _from == '中断中' and to in {'実施中', '終了'}:
        return True
    return False

これだと、if文が煩雑で読みにくいです。


フェアじゃないのでちょっと変えます。

def 遷移可能かチェックする(_from: str, to: str) -> bool:
    if _from == '審査中' and to in {'差し戻し中', '承認済み'}:
        return True
    if _from == '差し戻し中' and to in {'審査中', '終了'}:
        return True
    if _from == '承認済み' and to in {'実施中', '終了'}:
        return True
    if _from == '実施中' and to in {'終了', '中断中'}:
        return True
    if _from == '中断中' and to in {'実施中', '終了'}:
        return True
    return False


先程のよりはスッキリしました。

ただ、この例だとどのような状態が存在するのかを管理するところがありません。

文字列で制御しているため、関係のない文字列が入力される可能性があります。

また、遷移可能かチェックする関数の他にも申請ステータスを使って計算や判定をする場合

文字列だと自由に書けてしまうため

関数1で書かれているステータスの種類と関数2で書かれているステータスの種類が違う…なんてことが起こりえます。


そこで、どのような種類のステータスがあるのかは、Enumクラスで管理することにします。

import dataclasses
import enum
from typing import (
    Dict,
    Set,
)


class 申請ステータス(enum.Enum):
    審査中 = enum.auto()
    差し戻し中 = enum.auto()
    承認済み = enum.auto()
    実施中 = enum.auto()
    中断中 = enum.auto()
    終了 = enum.auto()


@dataclasses.dataclass
class 申請ステータス遷移:
    allowed: Dict[申請ステータス, Set[申請ステータス]]

    def __init__(self) -> None:
        # 本当はFinalつけて再代入負不可にしたかったけど
        # Cannot redefine an existing name as final と言われてできませんでした。
        # init=Falseにしてもできませんでした。
        # allowed: Dict[申請ステータス, Set[申請ステータス]] = { ... } は
        # Dictがmutableであるため
        # mutable default <class 'dict'> for field allowed is not allowed となります。
        # PythonにImmutalbeなMappingがあれば良いのですがないようです。
        self.allowed = {
            申請ステータス.審査中: {
                申請ステータス.差し戻し中, 申請ステータス.承認済み
            },
            申請ステータス.差し戻し中: {
                申請ステータス.審査中, 申請ステータス.終了
            },
            申請ステータス.承認済み: {
                申請ステータス.実施中, 申請ステータス.終了
            },
            申請ステータス.実施中: {
                申請ステータス.終了, 申請ステータス.中断中
            },
            申請ステータス.中断中: {
                申請ステータス.実施中, 申請ステータス.終了
            },
        }

    def can_transit(
        self,
        _from: 申請ステータス,
        to: 申請ステータス
    ) -> bool:
        allowd_states: Final = self.allowed[_from]
        return to in allowd_states


これだと、どのような申請ステータスがあるのかをEnumクラスで一元管理しているので

申請ステータスを利用する時はこのクラスを使うようになります。

申請ステータスの種類が増えたり変更したら

申請ステータスのEnumクラスを変更すれば良いです。

文字列だと申請ステータスを利用している関数一つ一つを確認して変更する必要がありますが、それがなくなるので変更がより安全になります。



以上、Pythonenumモジュールの紹介でした。

使ったことがない方も、便利なのでぜひ使っていただけたら、と思います!

DjangoORMでウィンドウ関数を使おう

この記事はDjango Advent Calendar 2019の記事です。

Django Advent Calendar 2019 - Qiita

そして私の初のアドベントカレンダーです!

最近仕事でBigQueryで分析関数を使うことが多いので、

そのなかでもウィンドウ関数をDjangoのORMでも使えないかなと思い調べてみました。

ウィンドウ関数とは

データベース製品よっては分析関数と呼ぶところもあればウィンドウ関数と呼ぶところもあります。

この記事ではPostgreSQLを使います。PostgreSQLでは分析関数をウィンドウ関数と呼んでいるのでウィンドウ関数で統一します。

PostgreSQLの公式ドキュメントには下記のように説明されています。

ウィンドウ関数は現在の行に何らかとも関係するテーブル行の集合に渡って計算を行います。 これは集約関数により行われる計算の形式と似たようなものです。 とは言っても、通常の集約関数とは異なり、ウィンドウ関数の使用は単一出力行に行をグループ化しません。 行はそれぞれ個別の身元を維持します。 裏側では、ウィンドウ関数は問い合わせ結果による現在行だけでなく、それ以上の行にアクセスすることができます。

3.5. ウィンドウ関数

例えば、このようなテーブルがある場合。

テーブル定義

    Column     |  Type   | Collation | Nullable |               Default                
---------------+---------+-----------+----------+--------------------------------------
 id            | integer |           | not null | nextval('salaries_id_seq'::regclass)
 department_id | integer |           | not null | 
 salary        | integer |           | not null | 
 employee_id   | integer |           | not null | 
Indexes:
    "salaries_pkey" PRIMARY KEY, btree (id)
    "salaries_departm_f58eef_idx" btree (department_id)
    "salaries_employe_a009a1_idx" btree (employee_id)


通常の集約関数で部署ごとの平均給与を出すとこのようになります。

SELECT
    department_id,
    ROUND(AVG(salary), 0) as avg_salary
FROM
    public.salaries
GROUP BY
    department_id
ORDER BY
    avg_salary DESC


出力結果例

 department_id | avg_salary 
---------------+------------
            16 |    9737374
            12 |    9694656
            15 |    9647917
            11 |    9588957
             5 |    9338559
            14 |    9337349
             6 |    9123184
             8 |    9070103
            13 |    9063918
            10 |    9020534
             9 |    8948148
            20 |    8934615
             4 |    8929108
            18 |    8712230
            17 |    8695489
            19 |    8669903
             2 |    8596131
             3 |    8578168
             7 |    8551880
             1 |    6790198


平均給与を出すだけなら良いですが、一人ひとりの社員の給与とその社員がその部署の中で何番目に給与が高いのかも出したいとします。

出力結果例

 department_id | employee_id |  salary  | rank 
---------------+-------------+----------+------
             1 |         414 | 40000000 |    1
             1 |         255 | 30000000 |    2
             1 |         316 | 20000000 |    3
             2 |         342 | 60000000 |    1
             2 |         673 | 50000000 |    2
             2 |         359 | 40000000 |    3
             3 |         409 | 60000000 |    1
             3 |         476 | 20000000 |    2
             3 |         410 | 10000000 |    3


このような結果を出すには上記のような単純なクエリでは出力できないので工夫が必要です。

そういう要望がある時に、ウィンドウ関数を知っていると、ウィンドウ関数を使わない場合よりもシンプルなクエリで実現できるまたはクエリのコストを抑えられることが多々あります。

ウィンドウ関数を使うとこのようなクエリになります。


SELECT
    department_id,
    employee_id,
    salary,
    rank() OVER (PARTITION BY department_id ORDER BY salary DESC)
FROM public.salaries
;


ウィンドウ関数なしだとこうまります。ウィンドウ関数を使ったクエリに比べてやや複雑です。

SELECT
    salaries.department_id,
    salaries.employee_id,
    salaries.salary,
    (
        SELECT COUNT(*)
        FROM public.salaries as rank
        WHERE
            rank.department_id = salaries.department_id
            AND rank.salary > salaries.salary
    ) + 1,
    rank() OVER (PARTITION BY department_id ORDER BY salary DESC)
FROM public.salaries
;


また、コストもレコード数が10000件のテーブルに関して

ウィンドウ関数を使ったクエリだと1024だったのに対し

ウィンドウ関数なしのクエリだと2072289でした。およそ206倍です。


rank()を説明しますと、PARTITION BYで部署ごとに分け、ORDER BYで部署の中でsalaryが多い順に順番を振り分けます。

ウィンドウ関数のPARTITION BYやORDER BYでどのようにテーブルを振り分けているかについては、下記のBigQueryのドキュメントの図1がわかりやすいです。

BigQueryなので分析関数と書いていますが、PostgreSQLのウィンドウ関数も同じ仕組みです。

cloud.google.com


DjangoORMでWindow関数を使うには

実行環境

* Python 3.8.0
* PostgreSQL 11.4
* Django 2.2.6
* psycopg2-binary 2.8.4


今回使用するモデル(テーブル定義は先程記述したテーブルと同じになります)

models.py

from django.db import models


class Salary(models.Model):
    # 面倒がってFK貼っていませんが実運用ではemployees  departmentsテーブル作ってFK貼ると思います
    employee_id = models.IntegerField()
    department_id = models.IntegerField()
    salary = models.IntegerField()

    class Meta:
        db_table = 'salaries'
        indexes = [
            models.Index(fields=['employee_id']),
            models.Index(fields=['department_id']),
        ]
    
    def __str__(self):
        return f'{self.department_id}_{self.employee_id}'


ウィンドウ関数をORMで表現するには、annotateを使います。

先ほどのクエリをORMで表現すると、下記のようになります。

from django.db.models.functions import Rank
from django.db.models import F, Window

from employee.models import Salary

window =  {
     'partition_by': [F('department_id')],
     'order_by': F('salary').desc()
}


Salary.objects.annotate(
    rank=Window(expression=Rank(), **window)
).values(
    'department_id',
    'employee_id',
    'salary',
    'rank'
)


F()は、モデルのフィールドを表すオブジェクトです。この例の場合、partition_byのdepartment_idとorder_byのsalaryです。

F()式のいいところは、実際にデータベースから値を取り出してPythonのメモリに格納しなくてもモデルのフィールドを参照できるところです。

https://docs.djangoproject.com/ja/2.2/ref/models/expressions/#f-expressions


annotateでウィンドウ関数でどのような項目を出力したいかしていします。引数expressionにDenseRank()を入れると、変数windowで指定したpartition byとorder byの通りにランキングされます。


この例では、rankという名前のアトリビュートにランキングを出力するように書いています。

https://docs.djangoproject.com/ja/2.2/ref/models/database-functions/#denserank


valuesで出力するアトリビュートを指定します。

valuesを指定しないと、id, department_id, employee_id, salary, rankの全てが出力されます。


Django Debug Toolbarをインストールし、

python manage.py debugsqlshellで上記のSQLを実行すると、

ウィンドウ関数が利用されたことを確認できました!

SELECT "salaries"."id",
       "salaries"."employee_id",
       "salaries"."department_id",
       "salaries"."salary",
       RANK() OVER (PARTITION BY "salaries"."department_id"
                    ORDER BY "salaries"."salary" DESC) AS "rank"
FROM "salaries"

https://docs.djangoproject.com/ja/2.2/ref/models/expressions/#window-functions


おまけ

ウィンドウ関数を紹介しましたが、場合によってはウィンドウ関数で表現できるけれどもウィンドウ関数を使わないほうが良い場合があります。


例えば、社員の給与と一緒にその社員が所属している部署の平均給与を表示したい場合です。

下記のような出力結果を期待します。

 department_id | employee_id | salary  | avg_salary 
---------------+-------------+---------+------------
             4 |        1707 | 8999649 |    5492152
             9 |        8296 | 8999531 |    5575559
             1 |        4641 | 8999048 |    5468614
            17 |         222 | 8998863 |    5376142
            19 |        4686 | 8997529 |    5449444
            10 |        1768 | 8994513 |    5588093
            17 |        7013 | 8994161 |    5376142
            15 |        6940 | 8994098 |    5535015
            11 |         577 | 8992925 |    5566160
            10 |        8139 | 8992773 |    5588093
             3 |        7718 | 8992511 |    5595843


ウィンドウ関数を使うと、下記のようなSQLになります。


SQLクエリ

SELECT
    department_id,
    employee_id,
    salary,
    rank() OVER (PARTITION BY department_id ORDER BY salary DESC)
FROM public.salaries
;


ORMクエリ

from django.db.models import F, Window, Avg

from employee.models import Salary


window =  {
    'partition_by': [F('department_id')],
    'order_by': F('salary').desc()
}


Salary.objects.annotate(
    avg_salary=Window(expression=Avg('salary'), **window)
).values(
    'department_id',
    'employee_id',
    'salary',
    'avg_salary'
)


ウィンドウ関数を使わないと、こうなります。

SQLクエリ

SELECT
    salaries.department_id,
    salaries.employee_id,
    salaries.salary,
    salary_averages.avg_salary
FROM
    public.salaries
JOIN
    (
    SELECT
        department_id,
        ROUND(AVG(salary), 0) as avg_salary
    FROM
        public.salaries
    GROUP BY department_id
) as salary_averages
ON
    salaries.department_id = salary_averages.department_id
ORDER BY
    salaries.salary DESC
;


ORMクエリは諦めました...

こういうの見つけたのですが、普通にSQL書いたほうが楽だと思ってしまいました。↓

Self join with django ORM - Stack Overflow


部署ごとの平均給与を出すクエリまではできました。給与が高い順です。

from employee.models import Salary


Salary.objects.values('department_id').annotate(avg_salary=models.Avg('salary')).order_by('-salary')


10000件レコードを入れてEXPLAINでコストを比較したところ、

①のウィンドウ関数を使ったクエリだと1024になったのに対し ②のJOINで対応したクエリだと393になりました。

ウィンドウ関数は便利ですが、その時々で出力したい結果に応じて使い分けようという話でした!


次回は、xKxAxKxさんです!

よろしくお願いします。

「データアーキテクト(データ整備人)を”前向きに”考える会」参加レポート

ブログ枠はブログが書くまでが勉強会ということで書きました!

イベントページ

analytics-and-intelligence.connpass.com


「データアーキテクト(データ整備人)の概観とこれからの展望と課題」 しんゆう さん (フリーランス

発表資料

speakerdeck.com

概要

  • データエンジニアはデータを集めてデータレイクに入れる人。ログ、バッチ処理などをする

  • データエンジニアとアナリストの間にあるのは

  • データの抽出

  • ダッシュボードなどでデータを可視化
  • データの整理。イレギュラーなデータへの対応や仕様変更への対応や監視や正確性の担保

  • 抽出、集計、管理は雑用ではなく専門家として役割を確立させるべき -> しんゆうさんは「データ整備人」という風に名付けた

  • 分析の経験がないと抽出の依頼に対して提案ができない

  • データ抽出はエンジニア領域に近い部分はあるものの、それが本業ではないので非エンジニアが対応すべき

  • 「次にデータを使う人が速やかに業務に遂行できるようにデータを使いやすくする人」

感想

  • 懇親会での話や発表を聞く限り、エンジニアやアナリストがデータ整備人を兼務していてデータ整備業は彼らの本分ではないのでモヤモヤしているという意見が多かったので、データ整備専門チームがある今の現場は恵まれていると思った

  • 分析の経験が自分にはないので、適切な提案できるデータ整備人ではないのが痛いところ


「3社の事例から学ぶ!現場で使われるダッシュボードの作り方」 ゆずたそ さん

発表資料

speakerdeck.com

概要

  • メルカリをサポートしている

  • メルカリはデータ整備人のポジションを最近作った

  • ダッシュボードは運用設計してちゃんと運用開始後にも見直すべし

  • 5W1Hを掘り下げてダッシュボード作ろう

  • ダッシュボード自体に対してPDCAまわそう

  • 5W1Hは焦点を小さく絞ろう。このくらい細かい粒度で↓

    • Who - 経営陣(Aさん、Bさん、Cさん)
    • When - 毎週水曜日の16時から
    • Where - 会議室Aで
    • Why - サービス利用状況を知るために
    • What - 主導線UU率の推移を
    • How - 議事録テンプレのURL経由で見る
  • 社内でSlackをどのように扱っているかのダッシュボードの例ー>Slackbotでオペレータに月イチで知らせる。ちゃんと運用される仕組み作り。

  • 「誰が」「いつ」「どこで」使うのか説明できるダッシュボードにしよう。誰かがいつか使ってくれるかもしれないのは結局使われない。

  • データを可視化するのはビジネスを助けるため

感想

  • データの可視化によってビジネス側が問題の原因となりうる事象に気付き、ビジネスルールを改善するという事例があった。データの可視化によってビジネスが改善される良い事例だった。メディア広告の例↓

https://speakerdeck.com/yuzutas0/20191127?slide=32

  • 5W1Hがとても細かくて驚いたけれども、そのくらい具体的で焦点が絞られていた方がちゃんと使われるかもしれない

  • データ整備の目的はデータが意思決定やビジネスの改善に貢献できるようにすること、というのが伝わる発表だった


「テータ整備業でぶつかった5つの課題_テータ整備人に求められる3つのスキル」 shinaro iwai さん(株式会社オプト)

発表資料

speakerdeck.com

概要

  • データ整備業をしている

  • クライアントからの依頼、社内からの依頼から求められていること: 意思決定の示唆、意思決定するために必要なことのすり合わせ

  • 雑なデータ取得依頼ー>なんのためにデータがほしいのかしつこく聞く

  • Google Colaboratory使うとSQLの処理の順序が可視化されてレビューしやすい

https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja

  • ノウハウが共有されなくて辛い->GitHubにクエリTipsを保存

  • データ整備人に必要なスキル

    • 課題抽出力。「これを出すことでどんなメリットがあるのか」「結果を用いて何をしようとしているのか」を考える能力。これがないとただのAPI
    • SQL。関数とか学ぼう
    • データ理解力。データの仕様、どのように取得され、どのように書こうされるか。SQLが使えることとデータの仕様を理解するのは別能力

感想

  • Google Colaboratory使ってレビューしてみたい(されてみたい)

https://speakerdeck.com/siwai/tetazheng-bei-ye-tehutukatuta5tufalseke-ti-tetazheng-bei-ren-niqiu-merareru3tufalsesukiru-sdyong?slide=24

  • 数値の正しさの感覚やデータ理解力が自分にはまだないので、もっとビジネスを知る必要があるのが自分の課題

  • 何のためにデータを使いたいのかをビジネス側とすり合わせることは、余計なことをしたくない自分たちのためにも、効果的にビジネスの改善をしたいビジネス側の人たちにとっても大事なこと


「サイエンス視点からのデータアーキテクト」 堀野将晴 さん (ヤフー株式会社)

発表資料

www.slideshare.net

  • データサイエンスではモデリング・分析のための前処理・可視化

  • モデリングからサービス実装までが1チーム。

  • 大きなデータなので前処理が必須だけれども時間もCPUも使って大変。共通データが必要

  • たくさんの部署と関わるのでコミュニケーション能力がとても大事

  • ドメイン知識も大事

  • データ開発運用をサービス側の開発に求めるのは失敗した。目標の違いやリソースが逼迫しているため。サイエンス側と協力して開発

  • ログ設計のルールは整備人と実装側の認識合わせが必要

  • 意図通りに使われないテーブル。中間テーブル作ったら大元のテーブルとジョインされた・・・。ー> 使われ方はよく確認してから作ろう

  • データ整備人で価値を出すには能動的に動くことと開発運用まで携わること

感想

  • データサイエンスに使われるデータための開発とサービス開発は根本的な目的が違うので、サービス側に任せずデータサイエンス側と開発するというのが印象的だっ た。サービス側でも開発できると思っていたので・・・

  • データ整備は誰もやりたがらないからこそ能動的に動くと価値があるというのもキャリアを考える上で参考になった


全体的なまとめ

  • データ整備業は雑用に思われがちだけれども他の役割と片手間で専門的な知識が必要な大事な役割

  • データ整備人に必要なスキル

    • 課題抽出力 - ビジネス上の課題を知ってビジネスの改善や意思決定に貢献できるようになろう
    • コミュニケーション能力 - データ取得を依頼するいろんな部署の人や外部の人と関わるため
    • ドメイン知識 - データがビジネスの改善や意思決定に使われるためにはそもそもビジネスを知っている必要がある

djangoでタイムゾーンとうまく付き合う

Djangogirls Tutorialで今まで何気なく書いていた、Postモデルのpublished_dateとcreated_dateで使うdjango.utils.timezone.now。これについて疑問に思ったことがあったので調べました。


対象

  • (レベル感)Djangoチュートリアルくらいの複雑さのものを自力で作ることができる

  • (問題)今のところ作りたいものは作れているけど正直タイムゾーンの扱いの仕組みをわかっていない人


1. shell上でPost.objects.get(pk=1).pulished_dateを出力するとUTC時間で出力されるけれども、テンプレートで描画したブラウザ上では日本時間でで出力されるのはなぜ?

Djangoでは、タイムゾーンサポートを有効にした場合(つまりsettings.pyでUSE_TZ=Trueとした場合、日時は

となります。

タイムゾーンについて、Pythonでは「native」と「aware」という概念があります。

以下、ざっくり、タイムゾーンなしの時間を「native」な時間、タイムゾーンありの時間を「aware」な時間とします。

nativeとawareについては下記の記事が分かりやすいです。

【Django】native timeをaware timeに変換する方法 | エンジニアの眠れない夜

公式ドキュメントはこちらです。

datetime --- 基本的な日付型および時間型 — Python 3.8.2 ドキュメント

例えば、Djangoでこんなモデルがあるとします。

models.py

from django.conf import settings
from django.db import models
from django.utils import timezone


class Post(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    published_date = models.DateTimeField(blank=True, null=True)

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    def __str__(self):
        return self.title

    class Meta:
        db_table = 'posts'


Djangoのshell機能で動きを確認すると・・・

>>> from django.contrib.auth.models import User
>>> from blog.models import Post
>>> 
>>> author = User.objects.create(username='dummy', email='dummy@example.com')
>>> post = Post.objects.create(
    author=author,
    title='dummy',
    text='dummy'
)


Postを日本時間で2019/10/10 19:00:00 に保存した場合、created_dateは下記のようになります。

  • データベース(PostgreSQL以外) 2019-10-10 10:00:00

  • データベース(PostgreSQL) 2019-10-10 10:00:00+00

Djangoでは、PostgreSQLだとdatetimeFieldはデータベース上ではtimestamp with time zoneとなり、タイムゾーンUTCとなる仕様です。

その他のデータベースだとタイムゾーンなしとなります。値はUTCの日時で保存されます。

# 上記省略
>>> post.created_date
datetime.datetime(2019, 10, 10, 10, 0, 0, tzinfo=<UTC>)

Djangoでは、タイムゾーンサポートを有効にした場合、datetimeオブジェクトはawareなオブジェクトとして扱われます。

内部的な処理にはタイムゾーンUTCのdatetimeオブジェクトを使い、 エンドユーザー入出力を行うレイヤー(テンプレートやフォームなど)では現地時間でやり取りするという思想のもと、このようになっています。

なぜフォームやテンプレートだけ現地時間で内部の処理はUTCかというと、 ユーザーが複数のタイムゾーンを使用しており、ユーザに対して彼らの時計と同じ日時を表示したい場合に内部はUTCにすると柔軟に時間の変換ができ、時間を扱うオブジェクトを処理する過程で値を間違えることを防げるからです。 また、UTCだとサマータイムを適用している場合に変換ミスが起こることを防ぎます。

とはいっても複数のタイムゾーン使っていないとイメージ湧きづらいと思うので、とりあえずDjangoではタイムゾーンを使うのがデフォルトなので よほどのことがない限りタイムゾーンサポートを有効にするべしということを抑えられれば、と思います。

詳しくは↓

https://docs.djangoproject.com/ja/2.2/topics/i18n/timezones/


2. 時間を検索条件に入れたい時、どんな形で時間のオブジェクトを渡すのが良いのだろう?

タイムゾーンを有効にしている場合、 タイムゾーンに指定したdatetimeオブジェクトかUTCタイムゾーンとしたdatetimeオブジェクトが良いと思います。 どちらかと言えばどちらでもいいというのが個人的な所感です笑

ケースバイケースですが、 DjangoのFormクラスのDatetimeFieldのcleaned_dataや、DRFのSerializerのDatetimeFieldのvalidated_dataでは 現地をタイムゾーンにしたdatetimeオブジェクトが出力されるので それらを使うならわざわざUTCに直してから使うこともないかなと思います。

forms.py

from django import forms


class SearchPostForm(forms.Form):
    from_date = forms.DateTimeField()
    to_date = forms.DateTimeField()


filterで検索してみる

>>> from blog.forms import SearchPostForm
>>> from blog.models import Post
>>>
>>>
>>> form = SearchPostForm({'from_date':'2019-10-10 10:00:00', 'to_date':'2019-10-10 11:00:00'})
>>> form.is_valid()
True
>>> form.cleaned_data
{'from_date': datetime.datetime(2019, 10, 10, 10, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>), 'to_date': datetime.datetime(2019, 10, 10, 11, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)}
>>>
>>> from_date = form.cleaned_data.get('from_date')
>>> to_date = form.cleaned_data.get('to_date')
>>>
>>> Post.objects.filter(published_date__gte=from_date, published_date__lte=to_date)[0].published_date
datetime.datetime(2019, 10, 10, 1, 0, tzinfo=<UTC>)


serializers.py

from rest_framework import serializers

class SearchPostSerializer(serializers.Serializer):
    from_date = serializers.DateTimeField()
    to_date = serializers.DateTimeField()


filterで検索してみる

>>> from blog.serializers import SearchPostSerializer
>>> from blog.models import Post
>>>
>>>
>>> serializer = SearchPostSerializer(data={'from_date':'2019-10-10T10:00:00+09:00', 'to_date':'2019-10-10T11:00:00+09:00'})
>>> serializer.is_valid()
True
>>> serializer.validated_data
>>> serializer.validated_data
OrderedDict([('from_date', datetime.datetime(2019, 10, 10, 10, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)), ('to_date', dat
etime.datetime(2019, 10, 10, 11, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>))])
>>>
>>> from_date = serializer.validated_data.get('from_date')
>>> to_date = serializer.validated_data.get('to_date')
>>>
>>> Post.objects.filter(published_date__gte=from_date, published_date__lte=to_date)[0].published_date
datetime.datetime(2019, 10, 10, 1, 0, tzinfo=<UTC>)


また、nativeなオブジェクトを使うとこんなwarningが出ます。

RuntimeWarning: DateTimeField received a naive datetime * to while time zone support is active.

Awareなオブジェクトを使う設定にしているのにnativeなの使うなよってことですね。

試しにnativeなオブジェクトで使ってみたところ、UTCとして扱われるような動きをしています。 Nativeオブジェクトだと意図しない検索結果になることがあるのでお勧めしません。


3. APIではどのように出力するのが良いだろう

テンプレートに時間を表示したい場合、自動でローカルタイムに変更されて表示されます。では、APIでは?

Postモデルの一覧を返したいとします。

Post.objects.get(pk=1)django.https.JsonResponse(またはHttpResponse)を使って返すと、UTCのままになります。

views.py

from django.forms.models import model_to_dict
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_http_methods

from blog.models import Post


@require_http_methods(['GET'])
def get(request, pk: int):
    post = get_object_or_404(Post, pk=pk)
    return JsonResponse(
        {
            'post': model_to_dict(post)
        }
    )


レスポンス

{"post": {"id": 1, "author": 1, "title": "not_published", "text": "dummy", "created_date": "2019-10-10T10:00:00Z", "published_date": null}}


現地時間でエンドユーザーに使ってもらいたい場合、JSを使ってフロント側で変換するか、view関数内で変換する必要があります。


① (JavaScriptを使ってエンドユーザーに時間を表示する場合)UTC時間でAPIは返してJavaScriptに現地時間に直してもらう

Moment.jsを使うとこんな感じになります。

Moment.js | Home

moment('2019-10-10T10:00:00.000Z').tz("Asia/Tokyo").format()
// ブラウザ上では`2019-10-10 19:00:00`となります。


フォーマットを変更するには

moment('2019-10-10T10:00:00.000Z').tz("Asia/Tokyo").format('YYYY年MM月DD日 HH時mm分SS秒’)
// ブラウザ上では` 2019年10月10日 19時00分00秒` となります。


タイムゾーンUTCのdatetimeオブジェクトをDjango.utils.timezone.localtimeで現地時間に変換する。

localtime()で現地をタイムゾーンにしたdatetimeオブジェクトに変換されます。

>>> from django.utils import timezone
>>>
>>>
>>> time = timezone.now()  # 2019-10-10 10:00:00 tzinfo=<UTC>とします
>>> timezone.localtime(time)
datetime.datetime(2019, 10, 10, 19, 00, 00, 00000, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)


③ (DRFを使っている場合)Serializerに現地時間に返してもらう。

上記と同じです。Serializerのvalidated_dataをResponseに入れて返します。

serializers.py

from rest_framework import serializers

from blog.models import Post


class PostModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ('author', 'title', 'text', 'published_date')
>>> from blog.models import Post
>>> from blog.serializer import PostSerializer
>>> 
>>> 
>>> post = Post.objects.get(pk=2)
>>> serializer = PostSerializer(post)
>>> serializer.data
{'author': 1, 'title': 'already_published', 'text': 'dummy', 'published_date': '2019-10-10T10:00:00+09:00'}
>>> # +09:00となっているのでタイムゾーンがAsia/Tokyoの時間とわかる。


なお、SerializerのDatetimeFieldはデフォルトでISO-8601フォーマットになっています。それ以外のフォーマットで出力したい場合、DatetimeFieldの引数formatを指定してください。

例えばこんな感じで

from rest_framework import serializers

serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S %Z")


Formatのドキュメント↓

https://www.django-rest-framework.org/api-guide/fields/#datetimefield-format-strings


どちらでも良いと思いますが、DRFがよしなにやってくれているので3が間違いが少なく楽かなと個人的には思います。

ただ、いろんなAPIのドキュメントやFAQを見ていると、 どのタイムゾーンの時間なのかはレスポンスに加えないと使う人がどのタイムゾーン時間なのか分からず困るAPIになるので注意しないとな、と思いました。


DropboxAPIは全てUTCなのでタイムゾーンの情報は書いていませんね。ドキュメントにもUTC使うよ!と書いてくれているのでレスポンスにタイムゾーンの情報がなくても「ああ、なるほどね」となります。

https://www.dropbox.com/developers/documentation/http/documentation#file_requests-get


GitHubAPIUTCですね。

https://developer.github.com/v4/scalar/datetime/


Connpassは現地時間ですね。 ISO-8601フォーマットで2012-04-17T20:30:00+09:00のような形で返されます。

https://connpass.com/about/api/