Введение в юридическую аналитику

Это руководство представляет собой обзор некоторых моих экспериментов с юридической аналитикой. Я надеюсь, что чтение этого резюме будет полезно для понимания машинного обучения в целом, а также для применения науки о данных в правовых системах. Мы будем очищать юридическую базу данных для данных и создадим двоичный классификатор для решений суда, чтобы предсказать, будет ли отклонено данное решение с помощью Python и PySpark. В этом подходе особое внимание будет уделено тому, как продвигался проект и как можно применить одни и те же концепции ко многим различным проектам в области машинного обучения.

Первый шаг к любому проекту машинного обучения — получение данных. Как правило, чем больше данных, тем лучше, но чем больше данных, тем лучше. Когда я начинал этот проект, я предполагал, что он будет использовать судебные документы по делам об опеке над детьми, чтобы предсказать, кто из родителей получит опеку. Однако, когда я начал изучать юридические базы данных и репозитории, оказалось, что данные трудно найти. Вместо этого наиболее доступной информацией в этих источниках были прошлые судебные решения. В определенных контекстах эти заключения были одним и тем же делом, рассмотренным другим судом. Например, Куигли против округа пожарной охраны Гарден-Вэлли — это решение, вынесенное Верховным судом Калифорнии в ответ на апелляцию Куигли против Гарден-Вэлли, решение, вынесенное апелляционным судом Калифорнии. Я решил, что эти данные кажутся более интересными и, возможно, более важными, чем документы об опеке над детьми, которые я искал. Таким образом, я перешел от прогнозирования результатов рассмотрения дел об опеке над детьми к прогнозированию того, будет ли решение апелляционного суда отменено Верховным судом.

Чтобы получить наши данные, мы создадим инструмент парсинга для автоматического сбора соответствующих дел из Интернета. Этот шаг также формирует данные, которые мы будем использовать для построения предиктора. Посмотрев в HTML пару разных юридических баз, я решил соскрести с Justia, потому что формат заключений Верховных судов идентичен формату решений апелляционных судов.

Нам предстоит сделать еще один выбор. Юстиа хранила мнения всех пятидесяти штатов, а также федеральных судов на нужном нам уровне. Нам нужно выбрать, какие из них собирать.

Мы будем использовать только суды Калифорнии. Почему? Машинное обучение часто является хорошим решением для выполнения задач, с которыми люди обычно не справляются, потому что не видят достаточного количества закономерностей, чтобы делать правильные прогнозы. Это тип приложения, которое мы преследуем. Поэтому мы хотим собрать набор данных с как можно большим количеством базовых шаблонов.

Предположим, что использование мнений только из одного штата обеспечивает большую согласованность признаков — характеристик, на которых наша модель будет основывать свои прогнозы, — которые я хотел извлечь из данных. В качестве примера этих особенностей предположим, что мнения, содержащие фразы «Сан-Диего» и «смертная казнь», часто меняются местами, потому что прокуроры Сан-Диего склонны настаивать на смертной казни гораздо чаще, чем прокуроры в других штатах, и, следовательно, эти мнения часто бывают обратными. Кроме того, скажем, конкретный судья в Сан-Диего склонен направлять присяжных в таких делах, что смертная казнь редко применяется в Калифорнии, и она редко приговаривает подсудимых к смертной казни, поэтому, в отличие от мнений других судей в Сан-Диего, ее мнения обычно подтверждаются по апелляции. Если бы эти посылки были верны, мы бы хотели, чтобы наш предсказатель усвоил закономерность, согласно которой Сан-Диего и смертная казнь — это фразы, которые повышают вероятность пересмотра данного дела, если только этот судья не вынес решение по рассматриваемому делу. Мы можем значительно усложнить пример, предположив, что наш дружественный судья также часто проявляет снисходительность в уголовных делах, не требующих смертной казни, и поэтому ее мнение часто меняется на противоположное, когда она выносит более мягкий приговор, чем смертная казнь. Это заставит нас учитывать судью, место и приговор одновременно, чтобы сформировать точный предиктор.

Наша модель может даже искать гораздо более сложные закономерности на основе схожих элементов, выделяя тенденции на основе ключевых слов. Однако предиктор может потерять эти важные закономерности в широком наборе данных. Тенденции могут даже полностью не проявляться, если мы исследуем всю федеральную апелляционную цепь, поскольку большее общее количество дел означает, что дела, относящиеся к модели, менее сконцентрированы и более разрознены. Даже если мы предположим, что добавление других данных напрямую не смешивает закономерности, которые мы хотим найти, это все равно усложнит вычислительную задачу извлечения этих закономерностей с помощью машинного обучения, поскольку они будут объяснять результат меньшей общей доли выборки. примеры в наборе данных. С другой стороны, сосредоточение внимания на Калифорнии позволяет нам ограничить данные настолько, чтобы поверить (или, по крайней мере, поверить с большей уверенностью), что закономерности очевидны и что они не затемнены смешиванием случаев в более широком масштабе.

В то же время, чем больше данных, тем лучше прогнозы, поэтому мы хотим найти как можно больше данных с согласованными закономерностями. Калифорния имеет самую большую систему судебных и апелляционных судов среди пятидесяти штатов, а также наибольшее число апелляционных дел в Юстии. С учетом общего количества примеров и того факта, что одна и та же судебная система наблюдает за апелляциями, имеет смысл ограничить наш набор данных делами, которые доходят до Верховного суда Калифорнии, и их соответствующими мнениями в апелляционных судах.

Когда этап планирования завершен, пришло время приступить к созданию парсера. Мы будем использовать Python и модуль для анализа HTML Beautiful Soup, у которого есть несколько полезных функций, которые значительно упрощают работу с веб-страницами.

from bs4 import BeautifulSoup
import urllib.request

Все усложняется, когда мы пытаемся найти всю необходимую информацию за один скоординированный проход. Во-первых, нам нужно перейти к списку страниц, содержащих заключения рассматриваемой судебной системы Калифорнии (Верховного или апелляционного суда). Эта страница представляет собой список лет, с 2002 по 2019 год, содержащий еще больше ссылок на дела, которые были рассмотрены в этих судах в том же году. Нам нужно открыть страницу за каждый год, открыть каждую ссылку на этой странице и собрать соответствующие данные о случаях на этой странице. Чтобы еще больше усложнить ситуацию, есть несколько страниц данных, связанных с некоторыми годами, очерченными страницей 1, страницей 2 и т. д. Нам нужно убедиться, что мы получаем записи с каждой страницы.

Структура страницы Justia выглядит следующим образом, иерархически:

Мы хотим начать с суда, будь то апелляционный суд или Верховный суд Калифорнии, и найти в нем все дела. Затем мы хотим сохранить соответствующие данные из каждого случая в один файл JSON для удобства использования на последующих этапах. Мы начнем с всеобъемлющей функции Python find_opinions(url). Эта функция принимает в качестве входных данных URL-адрес страницы со списком лет и возвращает список найденных записей случаев, организованных в хронологическом порядке в обратном порядке от последнего случая прошлого года до первого случая первого года. Мы вызовем две функции get_years(url) и scrape_years(url), которые нам еще предстоит реализовать. Первый будет собирать ссылки на разные годы на данной странице, а второй будет использовать ссылки, сгенерированные get_years(…), и собирать фактические судебные протоколы.

def find_opinions(url):
    collected_data = []
    # collect a list of the links to the pages with years
    year_urls = get_years(url)
    for year_url in year_urls:
        print("year url", year_url)
        # collect the data from the cases in that year
        year_results = scrape_year(year_url)
        collected_data.append(year_results)
    return collected_data

Эти две функции относительно просты в реализации. Я объясню get_years(…) здесь в качестве легкого введения в Beautiful Soup, и этого должно быть достаточно, чтобы собрать воедино аргументы в пользу scrape_years(…), который делает многие из тех же вещей. Конечно, если вас интересует полный код, вы можете посмотреть его на моем GitHub.

Цель этой функции — собрать список URL-адресов на странице, содержащих ссылки на обращения. Каждый из найденных URL-адресов будет определяться годом: с 2002 по 2019 год. Наш первый шаг — получить HTML-код с веб-страницы. Для этого мы будем использовать urllib для создания HTML-кода, который мы будем передавать в Beautiful Soup.

request = urllib.request.Request(url)
html = urllib.request.urlopen(request).read()
soup = BeautifulSoup(html,'html.parser')

Теперь, когда у нас есть HTML и анализатор, нам нужно знать, какую часть HTML мы хотим изолировать. Я считаю, что самый простой способ сделать это - просто в моем веб-браузере. Я ввел URL-адрес, который планировал использовать в качестве аргумента для get_years(…) в Opera, и проверил исходный код страницы.

Как видите, ссылки, которые нам нужны, 2019, 2018 и так далее, являются частью одной и той же таблицы с тегом ul с некоторыми модификаторами класса. Мы можем начать с выделения этой таблицы. В таблице каждая ссылка помечена буквой а. Мы хотим их всех.

main_table = soup.find(“ul”,
   attrs={‘class’:’list-columns list-columns-three list-no-styles’})
links = main_table.find_all(“a”)

Здесь есть небольшая заминка. В ссылках иногда отсутствует префикс «https://law.justia.com». В этом состоянии по ссылкам нельзя перейти напрямую. Однако мы можем просто перебирать ссылки и предоставлять этот контекст, если он отсутствует.

top_level = “https://law.justia.com"
year_urls = []
for link in links:
    url = link[‘href’]
    if not url.startswith(‘http’):
        url = top_level+url
    year_urls.append(url)

Вот и все! Все вместе функция выглядит так:

def get_years(url):
    request = urllib.request.Request(url)
    html = urllib.request.urlopen(request).read()
    soup = BeautifulSoup(html,’html.parser’)
    main_table = soup.find(“ul”,attrs=
         {‘class’:’list-columns list-columns-three list-no-styles’})
    links = main_table.find_all(“a”)
    top_level = “https://law.justia.com"
    #from each link extract the text of link and the link itself
    #List to store a dict of the data we extracted
    year_urls = []
    for link in links:
        print(link.text)
        url = link[‘href’]
        if not url.startswith(‘http’):
            url = top_level+url
        year_urls.append(url)
    return year_urls

Следующим шагом будет создание scrape_year(url). Я не буду описывать здесь процесс разработки, чтобы не повторяться, но мы создадим эту функцию с тем же мыслительным процессом, что и выше:

def scrape_year(url):
    top_level = “https://law.justia.com"
    request = urllib.request.Request(url)
    try:
        html = urllib.request.urlopen(request).read()
    except:
        print(“Failed to open link: “, url)
    soup = BeautifulSoup(html,’html.parser’)
    #look for the next link
    extracted_records = []
    next_page_records = None
    try:
        next_page = soup.find(“span”, class_=”next pagination page”)
        print(“next page: “, next_page)
        next_link = next_page.find(“a”)
        print(“next link: “, next_link)
        next_page_records = scrape_year(top_level + 
                                        next_link[‘href’])
    except:
        pass
    links = soup.find_all(“a”,class_=”case-name”)
    
    # From each link extract the text of link and the link itself
    # Store a dict of the data we extracted
    for link in links:
        title = link.text
        url = link[‘href’]
        if not url.startswith(‘http’):
            url = top_level+url
        record = {
            ‘title’:title[1:],
            ‘url’:url
        }
        # cases that are responses to opinions that have 
        #  not been appealed are not of interest to us
        if “In re” in record[‘title’]:
            continue
        extracted_records.append(record)
    for record in extracted_records:
        request = urllib.request.Request(record[‘url’])
        try:
            html = urllib.request.urlopen(request).read()
        except:
            print(“Fail collecting ”, record[‘url’])
        soup = BeautifulSoup(html,’html.parser’)
        case_decision = soup.find(“noframes”)
        case_summary = soup.find(“div”, 
                                 attrs={‘id’:’diminished-text’})
        try:
            record[‘opinion’] = case_decision.text
        except:
            record[‘opinion’] = “”
            print(“No opinion for: ”, record[‘title’])
        # Summaries are only sometimes present
        try:
            record[‘summary’] = case_summary.text
        except:
            record[‘summary’] = “”
    if next_page_records is None:
        return extracted_records
    extracted_records.extend(next_page_records)
    print(len(extracted_records))
    print(len(next_page_records))
    return extracted_records

Теперь все детали готовы. Мы можем собрать простую функцию main в качестве драйвера и назвать себя выполненным:

def main():
    print("Imports completed successfully")
    ca_supreme_court_url =          
            "https://law.justia.com/cases/california/supreme-court/"
    supreme_court_opinions = find_opinions(ca_supreme_court_url)
    with open('supreme_court_opinions.json', 'w') as outfile:
        json.dump(supreme_court_opinions, outfile, indent=4)
    ca_appeals_court_url = 
         "https://law.justia.com/cases/california/court-of-appeal/"
    appeals_court_opinions = find_opinions(ca_appeals_court_url)
    with open('appeals_court_opinions.json', 'w') as outfile:
        json.dump(appeals_court_opinions, outfile, indent=4)
    print("Scraping completed successfully")
if __name__ == "__main__":
    main()

Вот и все, что нужно для веб-скрейпинга! Если вы запускаете это самостоятельно, не удивляйтесь, если это займет где-то пару часов. Просто следите за регулярными обновлениями, и он должен позаботиться о себе. Оставайтесь с нами во второй части, в которой мы свяжем заключения Верховного суда с соответствующим заключением апелляционного суда.